Free Tutorial (Originally for Reid Draper)
Jun 16, 2014
From nothing to a simple DSL using Free Monads. First, some preliminaries:
> module DSL where > > import System.Exit > > type Error = String
First we need to define the constructs of our DSL. Normally we would define our language as a ‘vanilla’ recursive type. Something like:
data DSL = Print String DSL | GetChar (Char -> DSL) | Halt
This time we’ll define factoring out the recursion as a type parameter.
> data DSLf param = Print String param > | GetLine (String -> param) > | GetChar (Error -> param) (Char -> param) > | Halt
So our language can print a string, get a char and Halt. Simple enough.
Let’s show that our data-type is a Functor
> instance Functor DSLf where > fmap f Halt = Halt > fmap f (Print str cont) = Print str (f cont) > fmap f (GetLine next) = GetLine (f . next) > fmap f (GetChar k1 k2) = GetChar (f . k1) (f . k2)
Now let’s get back the data-type we would have gotten if we’d defined our language in the ‘vanilla’ style mentioned above. For this we need another data-type that can provide the explicit recursion
> data Free f a = Free (f (Free f a)) > | Pure a
Free takes two types, f and a. f must have kind * -> * and a kind *. Free is providing the same utility that fix does for functions, creating a recursive version of a type that was not recursive.
If we can guarantee that f is a functor, we can make any instantiation of
Free f a into a monad
> instance Functor f => Monad (Free f) where > return = Pure > Pure a >>= f = f a > Free x >>= f = Free (fmap (>>= f) x)
It’s okay if you don’t see the utility of this monad instance yet, later we’ll walk through how it works.
> type DSL = Free DSLf
Now we have our ‘proper’ DSL.
At this point we could call it a day and start using our DSL. The issue is that it would be ugly. We’d have to write programs in a very ugly style. Hello world would look like this:
hw :: DSL () hw = Free (Print "hello\n" (Free Halt))
Luckily we can abstract away the need to wrap everything in ’Free’s and ’Pure’s
> liftFree :: Functor f => f a -> Free f a > liftFree act = Free (fmap Pure act)
Now we can provide user-friendly functions for each of the constructs in our DSL.
> pr :: String -> DSL () > pr str = liftFree $ Print str ()
By the definition of liftFree, this gives us:
pr str = liftFree $ Print str () pr str = Free (fmap Pure (Print str ())) pr str = Free (Print str (Pure ())) -- Ignoring laziness (which is safe in this case)
The rest follow the same pattern and won’t be walked through
> gt :: DSL String > gt = liftFree $ GetLine id > > getC :: DSL (Either Error Char) > getC = liftFree $ GetChar (Left . id) (Right . id) > > hlt :: DSL a > hlt = liftFree Halt
DSL a is a monad (via the monad instance for Free) we can write programs using the do notation
> t1 = do > pr "hello\n" > str <- gt > pr $ "Hello again " ++ str
> isFree :: Free f a -> Bool > isFree (Pure _) = False > isFree (Free _) = True
Here is the kicker,
t1 is actually a data structure!
pr "hello\n" taken alone would look like:
Free (Print "hello\n" (Pure ()))
The whole things desugared out of do notation would look like:
1) Free (Print "hello\n" (Pure ())) >> 2) Free (GetLine (Pure . id)) >>= (\str -> 3) Free (Print ("Hello again " ++ str) (Pure ())))
Because of laziness, this structure stays as it is until something tries to inspect the first constructor. It looks like the first construct is the Free from line 1, but really the topmost expression is the function
(>>) So if you try to inspect the first constructor, you force the application.
Let’s give lines 2 + 3 the name ‘rest’ for now.
Using the definition of
>> for Free on line 1 we get:
Free (fmap (>>= (\_ -> rest)) (Print "hello\n" (Pure ())))
If you were needing the result of a function like ‘isFree’ then this is where reduction would stop.
If you were trying to interpret the DSL, then we would need to know which of the DSLf constructors was first inside the Free we’ve revealed. This forces the evaluation of the
fmap (>>= …
So, using the definition of fmap on DSLfs we get:
Free (Print "hello\n ((>>= (\_ -> rest)) (Pure ())))
> dummyStr = "This is a dummy value" > > dummyChr = Right 'k' > dummyChr2 = Left "Shh" > > interpDumb :: DSL a -> [String] > interpDumb (Pure a) =  > interpDumb (Free Halt) =  > interpDumb (Free (Print s n)) = s : interpDumb n > interpDumb (Free (GetLine k)) = interpDumb (k dummyStr) > interpDumb (Free (GetChar k1 k2)) = interpDumb $ either k1 k2 dummyChr2 > > t2 = do > str <- gt > pr $ "Hello again " ++ (take 2 str) > ch <- getC > case ch of > Left e -> pr e > Right c1 -> pr $ take 3 $ repeat c1 > > interpIO :: DSL a -> IO a > interpIO (Pure a) = return a > interpIO (Free Halt) = exitSuccess > interpIO (Free (Print s n)) = print s >> interpIO n > interpIO (Free (GetLine k)) = getLine >>= interpIO . k > interpIO (Free (GetChar k1 k2)) = do > ch <- getChar > interpIO $ either k1 k2 $ if ch == '\EOT' > then Left "Error" > else Right ch