Cheatsheet: Free Monad

Posted on July 11, 2017

The free monad allows you to build your own custom monad. This lets you use the do notation for whatever you want.

I find that a lot of the tutorials on the Free monad have too much build-up, and it’s hard to figure out what the code should actually be at the end.

This will start with the example, then move to explaining it.

{-# LANGUAGE DeriveFunctor #-}

import Control.Monad.Free (Free (..))

main :: IO ()
main = runOp exampleProgram

exampleProgram :: Free (IOOp String) ()
exampleProgram = do
  write "enter 'quit' to exit\n"
  inputLoop

inputLoop :: Free (IOOp String) ()
inputLoop = do
  write "enter some words> "
  text <- inputLn
  if text == "quit" then end
    else do
    let nWords = length $ words text
    write $ "you entered: " ++ show nWords ++ "\n"
    inputLoop

write :: a -> Free (IOOp a) ()
write s = Free $ Write s (Pure ())

inputLn :: Free (IOOp a) String
inputLn = Free $ Read Pure

end :: Free (IOOp a) ()
end = Free EndProgram

data IOOp b next
  = EndProgram
  | Read (String -> next)
  | Write b next
  deriving (Functor)

runOp :: Free (IOOp String) () -> IO ()
runOp (Free op) = case op of
  EndProgram -> return ()
  Read f -> do
    line <- getLine
    runOp (f line)
  Write s nextOp -> do
    putStr s
    runOp nextOp
runOp (Pure a) = return a

Import

This imports the Free data type (and all of its constructors) from the module Control.Monad.Free. This package isn’t installed by default. You can get it with cabal install free.

Example program

The main function creates the example program and passes it to the interpreter, runOp.

This program ask you for a line, prints out how many words were in that line, and repeats this until you enter “quit”.

Note that the IO operations write and inputLn are functions we define below. Also note that this doesn’t work with the IO monad. This code is entirely pure, and could be run without ever doing IO for real. This might be useful in a test, for example.

Monad functions

The trio - write, inputLn, and end - are the only functions that the monad provides. Think of them as the “API” of the monad. Note how they just construct instances of Free with instances of our IOOp type.

The Pure () in the Write command means that the write function returns () into the monad.

The Pure in the Read lifts the line read into the Free monad.

In real life, you might create an alias for the Free (IOOp a) b type to clean up the signatures a little bit.

IOOp

The IOOp data type defines the possible operations in the monad. The next field in some of the types refers to the “next” operation that will be run. Its type will end up being (Free IOOp a b).

Note how Read doesn’t hold a next directly, instead it holds a function that takes the input string and returns a next. That function is whatever program logic comes after the read operation.

runOp

runOp “interprets” the language into the IO monad. We could just as easily build an interpreter that gives all fake values for testing.

The (Pure a) branch isn’t used here. Pure is how values are returned from the Free monad.

DeriveFunctor

At the top we have the language flag {-# LANGUAGE DeriveFunctor #-}. This enables writing deriving (Functor) for the IOOp data type. If you don’t want to use language flags, it also works fine to just write the Functor implementation yourself. If you did, it would look like this:

-- what you'd write if you didn't have `deriving (Functor)`:
instance Functor (IOOp b) where
  fmap f EndProgram     = EndProgram
  fmap f (Write b next) = Write b (f next)
  fmap f (Read fnext)   = Read (f . fnext)