For instance, a computation involving commands to read and write from the prompt:
First we describe the "commands" of our computation as a Functor data type
{-# LANGUAGE DeriveFunctor #-}
data TeletypeF next
= PrintLine String next
| ReadLine (String -> next)
deriving Functor
Then we use Free
to create the "Free Monad over TeletypeF
" and build some basic operations.
import Control.Monad.Free (Free, liftF, iterM)
type Teletype = Free TeletypeF
printLine :: String -> Teletype ()
printLine str = liftF (PrintLine str ())
readLine :: Teletype String
readLine = liftF (ReadLine id)
Since Free f
is a Monad
whenever f
is a Functor
, we can use the standard Monad
combinators (including do
notation) to build Teletype
computations.
import Control.Monad -- we can use the standard combinators
echo :: Teletype ()
echo = readLine >>= printLine
mockingbird :: Teletype a
mockingbird = forever echo
Finally, we write an "interpreter" turning Teletype a
values into something we know how to work with like IO a
interpretTeletype :: Teletype a -> IO a
interpretTeletype = foldFree run where
run :: TeletypeF a -> IO a
run (PrintLine str x) = putStrLn *> return x
run (ReadLine f) = fmap f getLine
Which we can use to "run" the Teletype a
computation in IO
> interpretTeletype mockingbird
hello
hello
goodbye
goodbye
this will go on forever
this will go on forever