MonadFix and the Lazy and Strict State Monad

Posted on March 4, 2018

In this post, I will assume rudamentary familiarity with the different Monad instances of the lazy and strict state monad and MonadFix . If you are not familiar with these concepts or want to brush up your knowledge, I recommend Kwang Yul Seo’s post on the lazy and strict state monad and Will Fancher’s post on MonadFix .

Recently, llvm-hs-pure got a new API for building modules called IRBuilder which makes this process significantly more convenient by taking care of a lot of the necessary book keeping. In particular, the API is built upon a state monad that tracks variables and creates fresh variables as necessary, allows the use of monadic binds to refer to operators and more. In the context of LLVM references to variables or blocks often end up being circular, e.g., the branch instructions in the basic blocks in a loop will form a cycle referencing each other. While monadic binds can’t be recursive by default, MonadFix and the RecursiveDo extension lift this restriction and thereby allow for a very convenient API even in the presence of recursive definitions. For a more detailed blogpost on a very similar API, I recommend Lewis’ post on the ASM monad.

Recursive functions are another case where references end up being circular and thereby require MonadFix . Sadly, this usecase was completely broken in llvm-hs-pure as Pavol Klacansky noticed in a bugreport: All attempts to build modules this way led to an infinite loop and GHC’s infamous <<loop>> exception. After investigating this problem, I figured out that replacing the strict state monad by the lazy state monad solved the problem and lead to the expected behavior instead of an infinite loop. In the following, I’m going to present a simplified version of the problem and explain why the two versions differ.

We’ll start out by defining a very simple type representing the instructions in our program. For this example, we only need to instructions:

A Dummy instruction and a Reference instruction that refers to the result of another instruction by its name.

data Instr = Reference String | Dummy deriving Show

We can now define the Builder monad which is used to build the list of instructions. Builder is just a type synonym for a State monad with the state being a list of (String, Instr) pairs. We’ll also define runBuilder function that run a builder with an initial state consisting of an empty list of instructions and returns the final list.

type Builder a = State [(String, Instr)] a runBuilder :: Builder a -> [(String, Instr)] runBuilder a = execState a []

Emitting an instruction appends it to the list of instructions and returns the name of the instruction. We also define two convenience wrappers for emitting Dummy and Reference instructions.

emitInstr :: (String, Instr) -> Builder String emitInstr (n, i) = do modify (\instrs -> instrs ++ [(n, i)]) pure n dummy :: String -> Builder String dummy n = emitInstr (n, Dummy) reference :: String -> String -> Builder String reference n ref = do let instr = Reference ref emitInstr (n, instr)

Finally, we can define a very simple example program consisting of a Reference instruction and a Dummy instruction with the Reference instruction referencing the Dummy instruction which is defined later (that is why we need MonadFix and RecursiveDo here).

example :: Builder () example = mdo ref <- reference "ref" foo foo <- dummy "foo" pure ()

You can use the following definition for main to test this example.

main :: IO () main = print (runBuilder example)

This example will work with both the lazy and the strict state monad. However, if we change the definition of reference as shown below, running the example will result in an infinite loop.

reference :: String -> String -> Builder String reference n ref = do let instr = Reference ref case ref of !a -> emitInstr (n, instr)

Introducing the strict pattern match here might seem silly and in this isolated example it definitely is. However, in general it is definitely possible that the way an instruction is emitted depends on the reference and thereby requires a pattern match. In llvm-hs , the call instruction checks if the callee has a void return type which resulted in the issue mentioned above. To better understand why the strict and the lazy monad behave differently here, I am going to substitute the Monad and MonadFix instances and inline the definitions.

Let us start by removing the use of mdo and replace it by an explicit use of mfix .

example = do mfix $ \foo -> do ref <- reference "ref" foo foo' <- dummy "foo" pure foo' pure ()

Next, we can substitute the definition of mfix . Since State s a in transformers is defined as a StateT s Identity a , the definition can look a bit complicated. For this post, we are going to assume that State has not been defined as a transformer and provide definitions for this simplified version of State . You can see the recursion in mfix by a occuring both on the left and on the right of = .

newtype State s a = State { runState :: s -> (a, s) } mfix f = State (\s -> let (a, s') = runState (f a) s in (a, s'))

In the next step, we inline this definition of mfix .

example :: State [(String, Instr)] () example = do State $ \s -> let (foo, s') = runState (do ref <- reference "ref" foo foo' <- dummy "foo" pure foo' ) s in (foo, s') pure ()

Finally, we desugar do notation and inline reference , dummy and runState .

example :: State [(String, Instr)] () example = do State $ \s -> let (foo, s') = let (ref, s'') = case foo of !a -> ("ref", s ++ [("ref", Reference foo)]) (foo', s''') = ("foo", s'' ++ [("foo", Dummy)]) in (foo', s''') in (foo, s') pure ()

The above definition uses the bind implementation of the lazy state monad, for the strict state monad, we need to change the let statement to be strict in the tuple (note that pattern matches in let statements are lazy by default):

let !(ref, s'') = case foo of !a -> ("ref", s ++ [("ref", Reference foo)]) (foo', s''') = ("foo", s'' ++ [("foo", Dummy)]) in (foo', s''')

At this point, the difference becomes clear: For the strict state monad, forcing (foo, s') forces (ref, s'') which in turn ends up forcing foo which has not yet been computed so we run into an infinite loop. For the lazy state monad, the evaluation of the (ref, s'') tuple and thereby also the case statement on foo is lazy and thus we can first evaluate that foo = "foo" before evaluating the case statement and avoid the infinite loop.

Conclusion