• No results found

Why?

9.7

Monad Transformers

Often we want to “piggyback” monads on top of each other. For instance, there might be a case where you need access to both IO operations through the IO monad and state functions through some state monad. In order to accomplish this, we introduce

aMonadTransclass, which essentially “lifts” the operations of one monad into an- MonadTrans

other. You can think of this as stacking monads on top of eachother. This class has a

simple method:lift. The class declaration forMonadTransis: lift

class MonadTrans t where

lift :: Monad m => m a -> t m a

The idea here is thattis the outer monad and thatmlives inside of it. In order to execute a command of typeMonad m => m a, we firstliftit into the transformer. The simplest example of a transformer (and arguably the most useful) is the state

transformer monad, which is a state monad wrapped around an arbitrary monad. Be- state monad

fore, we defined a state monad as:

newtype State state a = State (state -> (state, a))

Now, instead of using a function of typestate -> (state, a)as the monad, we assume there’s some other monadmand make the internal action into something of typestate -> m (state, a). This gives rise to the following definition for a

state transformer: state transformer

newtype StateT state m a =

StateT (state -> m (state, a))

For instance, we can think ofmas IO. In this case, our state transformer monad is able to execute actions in the IO monad. First, we make this an instance ofMonadTrans:

instance MonadTrans (StateT state) where lift m = StateT (\s -> do a <- m

return (s,a))

Here, lifting a function from the realm ofmto the realm ofStateT statesimply involves keeping the state (thesvalue) constant and executing the action.

Of course, we also need to make StateT a monad, itself. This is relatively straightforward, provided thatmis already a monad:

instance Monad m => Monad (StateT state m) where return a = StateT (\s -> return (s,a))

StateT m >>= k = StateT (\s -> do (s’, a) <- m s

let StateT m’ = k a m’ s’)

fail s = StateT (\_ -> fail s)

The idea behind the definition ofreturnis that we keep the state constant and simply return the state/a pair in the enclosed monad. Note that the use ofreturnin the definition ofreturnrefers to the enclosed monad, not the state transformer.

In the definition of bind, we create a new StateTthat takes a statesas an ar- gument. First, it applies this state to the first action (StateT m) and gets the new state and answer as a result. It then runs thekaction on this new state and gets a new transformer. It finally applies the new state to this transformer. This definition is nearly identical to the definition of bind for the standard (non-transformer)Statemonad described in Section 9.3.

Thefailfunction passes on the call tofailin the enclosed monad, since state transformers don’t natively know how to deal with failure.

Of course, in order to actually use this monad, we need to provide functiongetT

,putT andevalStateT. These are analogous togetState, putStateand

getT putT evalStateT

runStateMfrom Section 9.3:

getT :: Monad m => StateT s m s getT = StateT (\s -> return (s, s)) putT :: Monad m => s -> StateT s m () putT s = StateT (\_ -> return (s, ()))

evalStateT :: Monad m => StateT s m a -> s -> m a evalStateT (StateT m) state = do

(s’, a) <- m state return a

These functions should be straightforward. Note, however, that the result ofevalStateT

is actually a monadic action in the enclosed monad. This is typical of monad trans- formers: they don’t know how to actually run things in their enclosed monad (they only know how toliftactions). Thus, what you get out is a monadic action in the inside monad (in our case, IO), which you then need to run yourself.

We can use state transformers to reimplement a version of ourmapTreeMfunction from Section 9.3. The only change here is that when we get to a leaf, we print out the value of the leaf; when we get to a branch, we just print out “Branch.”

mapTreeM action (Leaf a) = do

9.7. MONAD TRANSFORMERS 141

b <- action a return (Leaf b)

mapTreeM action (Branch lhs rhs) = do lift (putStrLn "Branch")

lhs’ <- mapTreeM action lhs rhs’ <- mapTreeM action rhs return (Branch lhs’ rhs’)

The only difference between this function and the one from Section 9.3 is the calls tolift (putStrLn ...)as the first line. Thelifttells us that we’re going to be executing a command in an enclosed monad. In this case, the enclosed monad is

IO, since the command lifted isputStrLn. The type of this function is relatively complex:

mapTreeM :: (MonadTrans t, Monad (t IO), Show a) => (a -> t IO a1) -> Tree a -> t IO (Tree a1)

Ignoring, for a second, the class constraints, this says thatmapTreeMtakes an action and a tree and returns a tree. This just as before. In this, we require thattis a monad transformer (since we applyliftin it); we require thatt IOis a monad, since we useputStrLnwe know that the enclosed monad isIO; finally, we require thatais an instance of show – this is simply because we useshowto show the value of leaves.

Now, we simply changenumberTreeto use this version ofmapTreeM, and the new versions ofgetandput, and we end up with:

numberTree tree = mapTreeM number tree where number v = do

cur <- getT putT (cur+1) return (v,cur)

Using this, we can run our monad:

MTrans> evalStateT (numberTree testTree) 0 Branch Branch Leaf ’a’ Branch Leaf ’b’ Leaf ’c’ Branch Leaf ’d’ Leaf ’e’ *MTrans> it

Branch (Branch (Leaf (’a’,0))

(Branch (Leaf (’b’,1)) (Leaf (’c’,2)))) (Branch (Leaf (’d’,3)) (Leaf (’e’,4)))

One problem not specified in our discussion of MonadPlus is that our search algorithm will fail to terminate on graphs with cycles. Consider:

cycles

gr3 = Graph [(0, ’a’), (1, ’b’), (2, ’c’), (3, ’d’)] [(0,1,’l’), (1,0,’m’), (0,2,’n’),

(1,3,’o’), (2,3,’p’)]

In this graph, there is a back edge from node b back to node a. If we attempt to run

searchAll2, regardless of what monad we use, it will fail to terminate. Moreover, if we move this erroneous edge to the end of the list (and call thisgr4), the result of

searchAll2 gr4 0 3will contain an infinite number of paths: presumably we only want paths that don’t contain cycles.

In order to get around this problem, we need to introduce state. Namely, we need to keep track of which nodes we have visited, so that we don’t visit them again.

We can do this as follows:

searchAll5 g@(Graph vl el) src dst | src == dst = do visited <- getT putT (src:visited) return [src] | otherwise = do visited <- getT putT (src:visited) if src ‘elem‘ visited then mzero else search’ el where search’ [] = mzero search’ ((u,v,_):es) | src == u =

(do path <- searchAll5 g v dst return (u:path)) ‘mplus‘ search’ es

| otherwise = search’ es

Here, we implicitly use a state transformer (see the calls togetTandputT) to keep track of visited states. We only continue to recurse, when we encounter a state we haven’t yet visited. Futhermore, when we recurse, we add the current state to our set of visited states.

Now, we can run the state transformer and get out only the correct paths, even on the cyclic graphs:

9.7. MONAD TRANSFORMERS 143

MTrans> evalStateT (searchAll5 gr3 0 3) [] :: [[Int]] [[0,1,3],[0,2,3]]

MTrans> evalStateT (searchAll5 gr4 0 3) [] :: [[Int]] [[0,1,3],[0,2,3]]

Here, the empty list provided as an argument toevalStateTis the initial state (i.e., the initial visited list). In our case, it is empty.

We can also provide anexecStateTmethod that, instead of returning a result, returns the final state. This function looks like:

execStateT :: Monad m => StateT s m a -> s -> m s execStateT (StateT m) state = do

(s’, a) <- m state return s’

This is not so useful in our case, as it will return exactly the reverse ofevalStateT

(try it and find out!), but can be useful in general (if, for instance, we need to know how many numbers are used innumberTree).

Exercises

Exercise 9.4 Write a functionsearchAll6, based on the code forsearchAll2, that, at every entry to the main function (not the recursion over the edge list), prints the search being conducted. For instance, the output generated forsearchAll6 gr 0 3should look like:

Exploring 0 -> 3 Exploring 1 -> 3 Exploring 3 -> 3 Exploring 2 -> 3 Exploring 3 -> 3 MTrans> it [[0,1,3],[0,2,3]]

In order to do this, you will have to define your own list monad transformer and make appropriate instances of it.

Exercise 9.5 Combine thesearchAll5function (from this section) with thesearchAll6

function (from the previous exercise) into a single function calledsearchAll7. This function should perform IO as in searchAll6but should also keep track of state using a state transformer.