“Write a stack-based interpreted language that includes: literals, arithmetic operations, variables, and control flow primitives. As a bonus, add asynchronous primitives such as fork and await.”
Fortunately, I was already familiar with the assignment because I implemented a statically typed programming language a year ago. Consequently, I decided to take this as a chance and do something different than the rest of the candidates. Spoiler: haskell + free monads!
The source code from this blog can be found here. My recommendation is to have both opened side-by-side.
The concept of a free monad comes from the field of Category Theory (CT). The formal definition is hard to grasp without a background in CT. In terms of programming, a more pragmatic definition is
Def. For any functor $f$, there exists a uniquely determined monad called the free monad of the functor $f$ 1
In plain words, we can transform any functor $f$ into a monad…And you may be wondering “how is that useful to write an interpreter in Haskell? You’ll see in a bit, stick with me.
Let’s show how free monads is translated into Haskell. We are going to base our work on the encoding of free monads of the haskell package free by Edward Kmett. The definition is as follows:
data Free f a = Pure a | Free (f (Free f a))
You can think of Free f
as a stack of layers f
on top of a value a
.
We will encode our interpreted language as a set of free monadic actions (instructions) represented as a datatype with a functorial shape. And our programs will be just a stack of thes instructions.
The interesting part about Free f
is that Free f
is a monad as long as f
is a functor:
instance Functor f => Monad (Free f) where
return = pure
Pure a >>= f = f a
Free m >>= f = Free ((>>= f) <$> m)
This will allow us to compose programs for free2. On top of that, do-notation gives us a nice syntax to compose ‘em all!
In the next section, we will finally see how to put this into practise.
When modeling a language3 using free monads, you usually divide the problem in two parts:
First, we need to define our language. We are free to model any kind of language, but the type that represents our language must be a functor. Otherwise, our language will not be able to be composed using bind (=<<)
.
Here is the sum type representing our byte code language:
type ByteCode = Free ByteCodeF
data ByteCodeF next
= Lit Value next
| Load Var next
| Write Var next
| BinaryOp OpCode next
| Loop (ByteCode Bool) (ByteCode ()) next
| Ret (Value -> next)
| NewChan (Channel -> next)
| Send Channel next
| Recv Channel next
| Fork (ByteCode ()) (Future () -> next)
| Await (Future ()) next
deriving (Functor)
data Value
= B Bool
| I Integer
deriving stock (Eq, Show, Ord)
newtype Chan a = Chan { getChan :: MVar a}
deriving newtype (Eq)
data Future a where
Future :: Exception e => Async (Either e a) -> Future a
Lit
instruction to instantiate literals (integer and boolean values).Load
/Write
instructions to load and write variables, respectively.BinaryOp
instruction to apply binary operations (+, *, <) on the top values of the stack.Loop
instruction to execute a set of instructions iteratively based on a certain condition.Ret
instruction to stop the program and return the value on top of the stack.Fork
/Await
instructions to start and await an asynchronous program, respectively.NewChan
/Send
/Recv
instructions to create a new asynchronous channel, send and receive a value through the channel, respectively. This channel allow to independent asynchronous programs to communicate.The careful reader may have notice that our type ByteCodeF
has a type parameter next :: Type
. This type parameter is required for ByteCodeF
to be a functor. We can think of next
as the next instruction in our program. In fact, when we interpret the ByteCode
free monad, next will be replaced by the (evaluated) next instruction of our program.
Next, for each constructor of our ByteCodeF
, we need a function that lifts ByteCodeF
into ByteCode
. For example, here is the definition of that function for Lit
:
lit :: Value -> ByteCode ()
lit v = Free (Lit v (Pure ()))
Implementing this for each constructor is tedious and error prone. This problem can be solved with metaprogramming. In particular, with template haskell (for an introduction to template haskell see my blog post). Fortunately, this is already implemented in Control.Monad.Free.TH.
Therefore, we are going to use makeFree
to automatically derive all these free monadic actions
$(makeFree ''ByteCodeF)
which generates the following code4
lit :: Value -> ByteCode ()
load :: Var -> ByteCode ()
write :: Var -> ByteCode ()
loop :: ByteCode Value -> ByteCode () -> ByteCode ()
newChan :: ByteCode Channel
send :: Channel -> ByteCode ()
recv :: Channel -> ByteCode ()
fork :: ByteCode () -> ByteCode (Async ())
await :: Async () -> ByteCode ()
Once we have the basic building blocks of our language, we can start building programs by composing smaller programs using monadic composition!
loopN :: Integer -> ByteCode ()
loopN until = do
let {n = "n"; i = "i"}
litI until
write n
litI 1
write i
loop (i < n) (i ++)
(<) i n = do
load n
load i
lessThan
ret
(++) i = do
litI 1
load i
add
write i
Now that we have the building blocks to construct our domain specific programs, it is only remaining to implement an interpreter to evaluate this embedded DSL in our host language Haskell.
The interpreter is usually implemented using iterM, which allows us to collapse our Free f
into a monadic value m a
by providing a function f (m a) -> m a
(usually called an algebra) that operates on a single layer of our free monad. If you are familiar with recursion schemes, iterM
is a specialization of cataA. This may sound confusing at first, but it is easier than it sounds.
First of all, we need to choose the monadic value m
. For our interpreted language, we choose m ~ Interpreter
newtype Interpreter a = Interpreter
{ runInterpreter :: StateT Ctx (ExceptT Err IO) a }
data Ctx = Ctx
{ stack :: [Value],
variables :: Map Var Value
}
data Err
= VariableNotFound Var
| StackIsEmpty
| BinaryOpExpectedTwoOperands
| AsyncException Text
| WhoNeedsTypes
where Ctx
is the current context of our program i.e. the state of the stack and the memory registers.
Then, we specialize iterM
to our example
-- <---------------- algebra ----------------->
iterm :: (ByteCodeF (Interpreter a) -> Interpreter a) -> ByteCode a -> Interpreter a
The last step and usually the most difficult one is to implement the algebra of our DSL
algebra :: ByteCodeF (Interpreter a) -> Interpreter a
algebra = \case
Ret f -> popI >>= f
Lit i k -> pushI i >> k
Load var k -> loadI var >>= pushI >> k
Write var k -> popI >>= storeI var >> k
BinaryOp op k ->
do
catchError
(liftA2 (applyOp op) popI popI >>= either throwError pushI)
( \case
StackIsEmpty -> throwError BinaryOpExpectedTwoOperands
e -> throwError e
)
>> k
Loop cond expr k ->
fix $ \rec -> do
b <- interpret cond
if b
then interpret expr >> rec
else k
NewChan f ->
liftIO getChan >>= f
Send chan k ->
popI >>= (liftIO . sendChan chan) >> k
Recv chan k ->
liftIO (recvChan chan) >>= pushI >> k
Fork branch k ->
future branch >>= k
Await (Future async') k -> do
ea <- liftIO $ waitCatch async'
case ea of
Left (SomeException ex) -> throwError (AsyncException (T.pack $ show ex))
Right (Left ex) -> throwError (AsyncException (T.pack $ show ex))
Right _r -> k
Finally, we combine iterM
and algebra
to obtain
interpret :: ByteCode a -> Interpreter a
interpret = iterM algebra
which allow us to interpret our embedded language in our host language. The result of interpret
can be composed with other effectful programs and it can also be evaluated using runByteCode
:
runByteCode :: ByteCode a -> Either Err a
runByteCode = unsafePerformIO . runExceptT . flip evalStateT emptyCtx . runInterpreter . interpret
So far, we have built the following components:
lit
, load
, write
, loop
…interpret
and runByteCode
.Now that we have all the ingredients to create embedded stack-based programs and interpret them, we can put this into practise.
program :: ByteCode Value
program = do
chan1 <- newChan
chan2 <- newChan
_ <- fork $ do
loopN 100000
litI 1
send chan1
_ <- fork $ do
loopN 100000
litI 1
send chan2
loopN 10
recv chan1
recv chan2
add
ret
main :: IO ()
main = case runByteCode program of
Left err -> throw err
Right res -> putStrLn $ "Result = " <> show res
The following program
creates two asynchronous tasks that after a period of time, return the integer 1
through an asynchronous channel. The main program waits for these two asynchronous tasks to finish and outputs the sum of the results of the asynchronous tasks.
In this post, we have introduced free monads and how they can be used to implement embedded domain specific languages. In particular, we have seen how to embed a stack-based language in Haskell. To see other examples of domain specific languages, we refer the reader to the examples of the free package.
This post only covered a half of the free package. In the next post of this series, we’ll explore the other half: church encoding, applicative free, cofree…
The term “free” in the context of CT refers to the fact that we have not added structure to the original object. Don’t confuse the term with the adjective free: “costing nothing”. ↩
Pun intended. ↩
Technically you are defining an embedded domain specific language (eDSL). ↩
The actual signatures use MonadFree which is an mtl-style class that allow us to compose FreeT with other monad transformers. We were able to monomorphize the return value MonadFree f m => m ~ Free ByteCodeF
thanks to the instance Functor f => MonadFree f (Free f)
. ↩
Template Haskell (TH) is an extension of GHC that allows the user to do type-safe compile-time meta-programming in Haskell. The idea behind template haskell comes from the paper “Template Meta-programming for Haskell” by S.P. Jones and T. Sheard. Template Haskell was shipped with GHC version 6.0. The compiler extension has evolved a lot since 2003 and its current state is well described at the GHC: User Manual and template-haskell package.
Initially, TH offered the ability to generate code at compile time and allowed the programmer to manipulate the abstract syntax tree (AST) of the program. The addition of new capabilities such as lifting, TExp , runIO, and quasiquoting opened a bunch of new use cases to explore.
In this blog post, we are going explore a bunch of interesting Template Haskell’s use cases:
This article assumes some familiarity with Haskell and, in particular, with Template Haskell. There are many well-written tutorials on the internet such as A Practical Template Haskell Tutorial or Template Haskell Tutorial. We strongly recommend reading the previously mentioned tutorials before continuing with the reading.
Before we start, these are the list of language extensions and imports that we are going to use in the following sections:
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}
import Codec.Picture
import Control.Applicative (ZipList (..))
import Control.Monad (replicateM, when)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Char (isSpace)
import Data.Data hiding (cast)
import Data.Foldable
import qualified Data.Text as T
import Data.Word
import Instances.TH.Lift ()
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Language.Haskell.TH.Syntax
import Network.HTTP.Req
import Text.Read (readMaybe)
import qualified Text.URI as URI
Automatic derivation of type class instances is one of many problems that TH can solve. Although this problem can also be solved by generics, the compilation times are usually longer. For this reason, template haskell is still the preferred way to generate type class instances at compile time.
Here we present an example of how to derive the type class Foldable for arbitrary datatypes.
data List a
= Nil
| Cons a (List a)
deriveFoldable ''List
The implementation is as follows:
data Deriving = Deriving { tyCon :: Name, tyVar :: Name }
deriving (Typeable)
deriveFoldable :: Name -> Q [Dec]
deriveFoldable ty = do
(TyConI tyCon) <- reify ty
(tyConName, tyVars, cs) <- case tyCon of
DataD _ nm tyVars _ cs _ -> return (nm, tyVars, cs)
NewtypeD _ nm tyVars _ c _ -> return (nm, tyVars, [c])
_ -> fail "deriveFoldable: tyCon may not be a type synonym."
let (KindedTV tyVar StarT) = last tyVars
putQ $ Deriving tyConName tyVar
let instanceType = conT ''Foldable `appT` foldl' apply (conT tyConName) (init tyVars)
foldableD <- instanceD (return []) instanceType [genFoldMap cs]
return [foldableD]
where
apply t (PlainTV name) = appT t (varT name)
apply t (KindedTV name _) = appT t (varT name)
genFoldMap :: [Con] -> Q Dec
genFoldMap cs = funD 'foldMap (genFoldMapClause <$> cs)
genFoldMapClause :: Con -> Q Clause
genFoldMapClause (NormalC name fieldTypes)
= do f <- newName "f"
fieldNames <- replicateM (length fieldTypes) (newName "x")
let pats = varP f : [conP name (map varP fieldNames)]
newFields = newField f <$> zip fieldNames (snd <$> fieldTypes)
body = normalB $
foldl' (\ b x -> [| $b <> $x |]) (varE 'mempty) newFields
clause pats body []
genFoldMapClause _ = error "Not supported yet"
newField :: Name -> (Name, Type) -> ExpQ
newField f (x, fieldType) = do
Just (Deriving typeCon typeVar) <- getQ
case fieldType of
VarT typeVar' | typeVar' == typeVar ->
[| $(varE f) $(varE x) |]
AppT ty (VarT typeVar') | leftmost ty == ConT typeCon && typeVar' == typeVar ->
[| foldMap $(varE f) $(varE x) |]
_ -> [| mempty |]
leftmost :: Type -> Type
leftmost (AppT ty1 _) = leftmost ty1
leftmost ty = ty
Have you ever written a function like snd3 :: (a, b, c) -> b
or snd4 :: (a, b, c, d) -> b
? Then, you can do better by letting the compiler write those boilerplate and error-prone functions for you.
Here we present an example of how to write an arbitrary-sized zipWith.
We have chosen zipWith
since it is a fairly simple function but complex enough to be used as the base to write more complex functions.
Here is a first implementation of zipWithN
:
zipWithN :: Int -> Q [Dec]
zipWithN n
| n >= 2 = sequence [funD name [cl1, cl2]]
| otherwise = fail "zipWithN: argument n may not be < 2."
where
name = mkName $ "zipWith" ++ show n
cl1 = do
f <- newName "f"
xs <- replicateM n (newName "x")
yss <- replicateM n (newName "ys")
let argPatts = varP f : consPatts
consPatts = [ [p| $(varP x) : $(varP ys) |]
| (x, ys) <- xs `zip` yss
]
apply = foldl (\ g x -> [| $g $(varE x) |])
first = apply (varE f) xs
rest = apply (varE name) (f:yss)
clause argPatts (normalB [| $first : $rest |]) []
cl2 = clause (replicate (n+1) wildP) (normalB (conE '[])) []
This implementation works fine but the compiler will warn
you about a missing signature on a top-level definition.
In order to fix this, we need to add a type signature to the generated term:
zipWithN :: Int -> Q [Dec]
zipWithN n
| n >= 2 = sequence [ sigD name ty, funD name [cl1, cl2] ]
...
where
...
ty = do
as <- replicateM (n+1) (newName "a")
let apply = foldr (appT . appT arrowT)
funTy = apply (varT (last as)) (varT <$> init as)
listsTy = apply (appT listT (varT (last as))) (appT listT . varT <$> init as)
appT (appT arrowT funTy) listsTy
...
Now you can use zipWithN
to generate arbitrary-sized zipWith
functions:
...
$(zipWithN 6)
$(zipWithN 7)
$(zipWithN 8)
...
Manually generating each instance partially defeats the purpose of zipWithN
.
In order to address this issue, we need the auxiliary function genZipWith
. Notice, we will use an alternative simplified definition of zipWithN
that exploits slicing and lifting to showcase.
genZipWith :: Int -> Q [Dec]
genZipWith n = traverse mkDec [1..n]
where
mkDec ith = do
let name = mkName $ "zipWith" ++ show ith ++ "'"
body <- zipWithN ith
return $ FunD name [Clause [] (NormalB body) []]
zipWithN :: Int -> Q Exp
zipWithN n = do
xss <- replicateM n (newName "xs")
[| \f ->
$(lamE (varP <$> xss)
[| getZipList
$(foldl'
(\ g xs -> [| $g <*> $xs |])
[| pure f |]
(fmap (\xs -> [| ZipList $(varE xs) |]) xss)
)
|])
|]
Now, we can generate arbitrary sized zipWith
functions
$(genZipWith' 20)
which will produce zipWith1
, zipWith2
, …, zipWith19
, zipWith20
.
Static input data is expected to be “correct” on a strongly typed programming language.
For example, assigning the decimal number 256
to a variable of type Byte
is expected to fail at compile time.
So, let’s try it on Haskell:
ghci> :m +Data.Word
ghci> 256 :: Word8
<interactive>:2:1: warning: [-Woverflowed-literals]
Literal 256 is out of the Word8 range 0..255
0
Ops! Indeed, the code has compiled with an unexpected overflow.
The keen-eyed reader may be thinking that this bug could have been prevented with the appropriate GHC flags -Wall
and -Werror
.
Here we present a more general approach to validate static input data from the user using quasiquoting. This example can be easily adapted to all sorts of input data.
word :: QuasiQuoter
word = QuasiQuoter
{ quoteExp = parseWord
, quotePat = notHandled "patterns"
, quoteType = notHandled "types"
, quoteDec = notHandled "declarations"
}
where notHandled things = error $
things ++ " are not handled by the word quasiquoter."
parseWord :: String -> Q Exp
parseWord (trim -> str) =
case words str of
[w] ->
case break (== 'u') w of
(lit, size) ->
case readMaybe @Integer lit of
Just int -> do
when (int < 0) $ fail "words are strictly positive"
case size of
"u8" -> castWord @Word8 ''Word8 int
"u16" -> castWord @Word16 ''Word16 int
"u32" -> castWord @Word32 ''Word32 int
"u64" -> castWord @Word64 ''Word64 int
_ -> fail ("size " <> size <> " is not one of {u8, u16, u32, u64}")
Nothing -> fail (lit <> " cannot be parsed as Integer")
_ -> fail ("Unexpected word: " <> str)
where
castWord :: forall a. (Show a, Bounded a, Integral a) => Name -> Integer -> ExpQ
castWord ty int
| int <= fromIntegral (maxBound @a) = sigE [|int|] (conT ty)
| otherwise = fail (pprint ty <> " is outside of bound: [0," <> show (maxBound @a) <> "]")
trim :: String -> String
trim = f . f where
f = reverse . dropWhile isSpace
The word
quasiquoter can validate static input data
ghci> [word| 255u8 |]
255 :: Word8
and emit a compilation-time error if the data is not valid
ghci> [word| 256u8 |]
<interactive>:315:7-15: error:
* GHC.Word.Word8 is outside of bound: [0,255]
* In the quasi-quotation: [word| 256u8 |]
Running arbitrary IO on compile-time is one of the features of TH. This allows the user to make compilation dependant on external conditions such as the database schema, the current git branch or a local file content.
In this last example, we are going to use runIO
and quasiquoting
to get static pictures from remote and local files at compile-time. Notice, this example has been simplified to avoid all the overhead of proper error handling.
DynamicImage
img :: QuasiQuoter
img = QuasiQuoter {
quoteExp = imgExpQ
, quotePat = notHandled "patterns"
, quoteType = notHandled "types"
, quoteDec = notHandled "declarations"
}
where notHandled things = error $
things ++ " are not handled by the img quasiquoter."
imgExpQ :: String -> ExpQ
imgExpQ str = do
uri' <- URI.mkURI (T.pack str)
bs <- runIO $ do
(bs, dynImg) <-
case useURI uri' of
Nothing -> BS.readFile str
Just (Left (url, _)) -> requestBs url
Just (Right (url, _)) -> requestBs url
either fail (pure . (bs,)) $ decodeImage bs
liftDynamicImage bs
liftDynamicImage :: ByteString -> Q Exp
liftDynamicImage bs = [| either error id (decodeImage bs) |]
requestBs :: Url scheme -> IO ByteString
requestBs url =
runReq defaultHttpConfig $ do
r <- req
GET
url
NoReqBody
bsResponse
mempty
return (responseBody r)
The img
quasiquoter can be used to load pictures as static data inside your binary
dynImg1 :: DynamicImage
dynImg1 = [img|https://httpbin.org/image/jpeg|]
dynImg2 :: DynamicImage
dynImg2 = [img|./resources/pig.png|]
During this blog post, we have seem some of the use cases of template haskell and how to implement them. We encourage the reader to use our examples to build new and more compelling use cases of template haskell and to share them with the community.
We hope you enjoyed this post and don’t forget to share your own use cases for template haskell in the comments.
]]>fizz :: Int -> String
fizz n | n `mod` 15 == 0 = "FizzBuzz"
| n `mod` 3 == 0 = "Fizz"
| n `mod` 5 == 0 = "Buzz"
| otherwise = show n
main :: IO()
main = traverse_ (putStrLn . fizz) [1..100]
One of the main problems of type-level programming in Haskell is that you cannot implement higher-order functions on the type level (see saturation restriction). Therefore, we cannot abstract as much as we do on the term level using traverse
and [1..100]
. There is an accepted proposal unsaturated type families but it still not merged into GHC.
Are we doomed to write a lot of boilerplate code as in @cercerilla
example? The answer is no, there is a workaround to this problem called defunctionalization. Defunctionalization translates higher-order functions into first-order functions:
apply
function interpret all of your symbols and produce the value you want.This idea of defunctionalization is implemented in the singletons and in first-class-families.
Below we present the implementation of Fizz Buzz on the type-level using the singletons
library(you can achieve a similar result using fcf
package). The implementation is many times smaller than the one implemented using ad hoc higher-order combinators and easier to understand since most of the complexity is moved away to the singletons’ library.
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE GADTs #-}
module FizzBuzz where
import Data.Singletons.TH
import Data.Singletons.Prelude
import Data.Singletons.TypeLits
$(singletons [d|
type Divisible n by = Mod n by == 0
type FizzBuzzElem :: Nat -> Symbol
type FizzBuzzElem n =
If
(Divisible n 15)
"FizzBuzz"
( If
(Divisible n 3)
"Fizz"
( If
(Divisible n 5)
"Buzz"
(Show_ n)))
|])
type FizzBuzz n = Fmap FizzBuzzElemSym0 (EnumFromTo 1 n)
-- |
-- >>> fizzBuzz @10
-- "[FizzBuzz,1,2,Fizz,4,Buzz,Fizz,7,8,Fizz]"
fizzBuzz :: forall (n :: Nat). (KnownSymbol (Show_ (FizzBuzz n))) => String
fizzBuzz = symbolVal (Proxy @(Show_ (FizzBuzz n)))
Type-level programming in Haskell is still a difficult task since the support for higher-order abstractions is still not available in the language. With the use of libraries such as singletons
we can make this task less difficult reusing many higher-order abstractions on the type level.
Given the following haskell script generate-random-samples.hs
that requires mwc-random
{-# LANGUAGE ScopedTypeVariables #-}
import System.Random.MWC
import Data.Vector.Unboxed
import Control.Monad.ST
main = do
vs <- withSystemRandom $
\(gen::GenST s) -> uniformVector gen 20 :: ST s (Vector Int)
print vs
… how do you run it without having to globally install the package or having to build a whole cabal project ?
One of the simplest approaches using nix is the following:
$ nix-shell --packages 'haskellPackages.ghcWithHoogle (pkgs: with pkgs; [ mwc-random ])'
nix-shell> runghc generate-random-samples.hs
In order to reuse the command so other people can run the script, you can add the following shell.nix
:
{ compiler ? "ghc881" }:
let
pkgs = import (builtins.fetchGit {
url = "https://github.com/NixOS/nixpkgs.git";
rev = "890440b0273caab258b38c0873c3fe758318bb4f";
ref = "master";
}) {};
ghc = pkgs.haskell.packages.${compiler}.ghcWithPackages (ps: with ps; [
mwc-random
]);
in
pkgs.stdenv.mkDerivation {
name = "ghc-env";
buildInputs = [ ghc pkgs.cabal-install ];
shellHook = "eval $(egrep ^export ${ghc}/bin/ghc)";
}
So now you only need to call $ nix-shell
to enter into a pure shell with a specific GHC version that includes all your dependencies:
$ nix-shell
nix-shell> runghc generate-random-samples.hs
The only issue is that you must be aware of how nix work in order to be able to run the script.
But these could be solved using bash shebangs in your haskell script:
#!/usr/bin/env nix-shell
#!nix-shell -i runghc
{-# LANGUAGE ScopedTypeVariables #-}
import System.Random.MWC
import Data.Vector.Unboxed
import Control.Monad.ST
main = do
vs <- withSystemRandom $
\(gen::GenST s) -> uniformVector gen 20 :: ST s (Vector Int)
print vs
So now, you can run your haskell script in an environment without ghc:
./generate-random-samples.hs
[6052359640365008112,3693984866634705670,6521947999724514858,640433474764908030,-4262896110044960033,-1795671341099353119,-2220462704949887998,-248182841640258167,709016591698961687,-3622504171575206589,5987258113070378446,-159251391303273987,-8449937247808153766,6165509553180365166,-8199532339362621783,-9187765480154042269,-2389922548196927048,-4842141643835297495,-1106748185069026877,826927505518387091]
After reading the following comment in reddit, I think it is worth mentioning that you can achieve better modularity in exchange of maintainability, by having everything on the haskell script, without having to depend on a shell.nix
file:
#!/usr/bin/env nix-shell
#!nix-shell -i runghc -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ mwc-random ])"
#!nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/d373d80b1207d52621961b16aa4a3438e4f98167.tar.gz
{-# LANGUAGE ScopedTypeVariables #-}
import System.Random.MWC
import Data.Vector.Unboxed
import Control.Monad.ST
main = do
vs <- withSystemRandom $
\(gen::GenST s) -> uniformVector gen 20 :: ST s (Vector Int)
print vs