My overdue post on free monads
Intuition
Just like how we can get a free Monoid ([]
) for a type t
.
We can get a free Monad for a functor F.
The methodology is the same, build up repeated contexts:
data [a] = [] | a : [a]
data Free f a = Pure a | Roll (f (Free f a))
Let’s see how we define the instances for Free
.
data Free f a = Pure a | Roll (f (Free f a))
class Functor f => Functor (Free f) where
fmap f (Pure a) = Pure $ f a
fmap f (Roll x) = Roll (fmap (fmap f) x)
class Functor f => Monad (Free f) where
return = Pure a
Pure a >>= f = f a
Roll x >>= f = concatFree (fmap f x)
where
--AKA join :: m (m a) -> m a
concatFree :: Functor f => Free f (Free f a) -> Free f a
concatFree (Pure x) = x
concatFree (Roll y) = Roll (fmap concatFree y)
-- E.g.
-- concatFree $ Roll (Identity (Pure (Roll (Identity (Pure a)))))
-- = Roll (fmap concatFree $ Identity (Pure (Roll (Identity (Pure a))))) -- Essence of why concatFree works
-- = Roll (Identity (concatFree $ Pure (Roll (Identity (Pure a)))))
-- = Roll (Identity (Roll (Identity (Pure a))))
Why is it useful?
As I was thinking how we can use DSLs to model computations e.g. IO, I came across free monads.
A familiarity with Monads is expected in this post.
In our case, we want to factor out impure parts from any code using free monads.
Free monads allows us to decompose any impure program into a pure representation of its behavior and a minimal impure interpreter.
Suppose we want to model IO parts:
-- | Taken from haskell for all (see references)
import Control.Monad.Free
import System.Exit hiding (ExitSuccess)
data TeletypeF x
= PutStrLn String x
| GetLine (String -> x)
| ExitSuccess
instance Functor TeletypeF where
fmap f (PutStrLn str x) = PutStrLn str (f x)
fmap f (GetLine k) = GetLine (f . k)
fmap f ExitSuccess = ExitSuccess
type Teletype = Free TeletypeF
putStrLn' :: String -> Teletype ()
putStrLn' str = liftF $ PutStrLn str ()
getLine' :: Teletype String
getLine' = liftF $ GetLine id
exitSuccess' :: Teletype r
exitSuccess' = liftF ExitSuccess
-- Our interpreter
run :: Teletype r -> IO r
run (Pure r) = return r
run (Free (PutStrLn str t)) = putStrLn str >> run t
run (Free (GetLine f )) = getLine >>= run . f
run (Free ExitSuccess ) = exitSuccess
echo :: Teletype ()
echo = do str <- getLine'
putStrLn' str
exitSuccess'
putStrLn' "Finished"
main = run echo
It seems like we paid a price however…? Our program is more much verbose… and… we still seem to have an error as shown below
import System.Exit
main = do x <- getLine
putStrLn x
exitSuccess
putStrLn "Finished" -- <-- oops!
There is no way to prove it. For instance exitSuccess could be redefined as exitSuccess = return ()
. That way "Finished"
still gets printed to stdout.