What is the difference between different orderings of the same monad transformers?

瘦欲@ 提交于 2019-12-20 08:58:02

问题


I am attempting to define an API to express a particular type of procedure in my program.

newtype Procedure a = { runProcedure :: ? }

There is state, consisting of a mapping of IDs to records:

type ID = Int
data Record = { ... }
type ProcedureState = Map ID Record

There are three basic operations:

-- Declare the current procedure invalid and bail (similar to some definitions of fail for class Monad)
abort :: Procedure ()
-- Get a record from the shared state; abort if the record does not exist.
retrieve :: ID -> Procedure Record
-- Store (or overwrite) a record in the shared state.
store :: ID -> Record -> Procedure ()

I have a few goals with these operations:

  • Procedures can make assumptions (unlike a raw Map.lookup call) about which records are available, and if any of their assumptions are wrong, the Procedure as a whole returns failure.
  • A series of Procedures can be chained together using <|> (from class Alternative) in order to fall back to Procedures that make different assumptions. (Similar to STM's orElse)

Given these goals, I believe I want some combination of the State and Maybe monads.

-- Which to choose?
type Procedure a = StateT ProcedureState Maybe a
type Procedure a = MaybeT (State ProcedureState) a

I can't figure out how the two orderings of Maybe and State will behave differently. Can anyone explain the difference in behavior between the two orderings?

Also, if you see a problem with my original thinking (perhaps I am over-engineering), feel free to point it out.

Conclusion: All three answers were helpful, but there was one common idea that helped me decide which ordering I wanted. By looking at the return type of runMaybeT/runStateT, it was easy to see which combination had the behavior I was looking for. (In my case, I want the return type Maybe (ProcedureState, a)).


回答1:


Edit: I originally got the cases backwards. Fixed now.

The difference between orderings of monad transformer stacks really only matters when you're peeling off layers of the stack.

type Procedure a = MaybeT (State ProcedureState) a

In this case, you first run the MaybeT, which results in a stateful computation which returns a Maybe a.

type Procedure a = StateT ProcedureState Maybe a

Here the StateT is the outer monad, which means that after running the StateT with an initial state, you'll be given a Maybe (a, ProcedureState). That is, the computation may have succeeded, or may not have.

So which you choose depends upon how you want to handle partial computations. With MaybeT on the outside, you'll always get some sort of returned state regardless of the computation's success, which may or may not be useful. With StateT on the outside, you guarantee that all stateful transactions are valid. From what you describe, I would probably use the StateT variant myself, but I expect either could work.

The only rule for monad transformer ordering is that if IO (or another non-transformer monad) is involved, it must be the bottom of the stack. Typically people will use ErrorT as the next lowest level if it's required.




回答2:


To complement the other answers, I'd like to describe how to figure this out in the general case. That is, given two transformers, what are the semantics of their two combinations?

I had a lot of trouble with this question when I was starting to use monad transformers in a parsing project last week. My approach was to create a table of transformed types that I consult whenever I'm unsure. Here's how I did it:

Step 1: create a table of the basic monad types and their corresponding transformers types:

transformer           type                  base type (+ parameter order)

---------------------------------------------------------------

MaybeT   m a        m (Maybe a)            b.    Maybe b

StateT s m a        s -> m (a, s)          t b.  t -> (b, t)

ListT    m a        m [a]                  b.    [] b

ErrorT e m a        m (Either e a)         f b.  Either f b

... etc. ...

Step 2: apply each monad transformer to each of the base monads, substituting in for the m type parameter:

inner         outer         combined type

Maybe         MaybeT        Maybe (Maybe a)
Maybe         StateT        s -> Maybe (a, s)      --  <==  this !!
... etc. ...

State         MaybeT        t -> (Maybe a, t)      --  <== and this !!
State         StateT        s -> t -> ((a, s), t)
... etc. ...

(This step is a bit painful, since there's a quadratic number of combinations ... but it was a good exercise for me, and I only had to do it once.) The key for me here is that I wrote the combined types unwrapped -- without all those annoying MaybeT, StateT etc. wrappers. It's a lot easier for me to look at and think about the types without the boilerplate.

To answer your original question, this chart shows that:

  • MaybeT + State :: t -> (Maybe a, t) a stateful computation where there might not be a value, but there will always be a (possibly modified) state output

  • StateT + Maybe :: s -> Maybe (a, s) a computation where both the state and the value may be absent




回答3:


Let's pretend that rather than using State/StateT to store your procedures' state, you were using an IORef in the IO monad.

A priori there are two ways you could want mzero (or fail) to behave in a combination of the IO and Maybe monads:

  • either mzero wipes out the entire computation, so that mzero <|> x = x; or
  • mzero causes the current computation to not return a value, but IO-type effects are preserved.

It sounds like you want the first one, so that the state set by one procedure is "unrolled" for the next procedure in a chain of <|>s.

Of course, this semantics is impossible to implement. We don't know whether a computation will invoke mzero until we run it, but doing so may have arbitrary IO effects like launchTheMissiles, which we can't roll back.

Now, let's try to build two different monad transformer stacks out of Maybe and IO:

  • IOT Maybe -- oops, this doesn't exist!
  • MaybeT IO

The one that exists (MaybeT IO) gives the mzero behavior that is possible, and the nonexistent IOT Maybe corresponds to the other behavior.

Fortunately you're using State ProcedureState, whose effects can be rolled back, rather than IO; the monad transformer stack you want is the StateT ProcedureState Maybe one.




回答4:


You'll be able to answer the question yourself if you try to write "run" functions for both versions - I don't have MTL + transformers installed so I'm not able to do it myself. One will return (Maybe a,state) the other Maybe (a,state).

Edit - I've truncated my response as it adds detail which might be confusing. John's answer hits the nail on the head.




回答5:


Summary: Different Stack Orders Yield Different Business Logic

That is to say, different monad transformer orders of the stack do not only affect the evaluation orders, but also the functionalities of programs.

When demonstrating the impact of orders, people usually use the simplest transformers such as ReaderT, WriterT, StateT, MaybeT, ExceptT. Different orders of them do not give dramatically different business logic, so it is hard to understand the impact clearly. In addition, some subsets of them are commutative, i.e., there is no functionality differences.

For demonstration purpose, I suggest to use StateT and ListT, which reveal the dramatic difference between transformer orders on monad stacks.

Background: StateT and ListT

  • StateT: State monad is well explained in For a Few Monads More. StateT just gives you a little bit more power -- using the monadic operations of its underlying m. It is sufficient if you know evalStateT, put, get, and modify, which are explained in many State monad tutorials.
  • ListT: List, a.k.a, [], is a monad (explained in A Fistful of Monads). ListT m a (in package list-t) gives you something similar to [a] plus all monadic operations of the underlying monad m. The tricky part is the execution of ListT (something comparable to evalStateT): there are lots of ways of execution. Think about different outcomes you care when using evalStateT, runStateT, and execState, the context of List monad has lots of potential consumers such as just go over them, i.e., traverse_, fold them, i.e., fold, and more.

Experiment: Understand the Monad Transformer Order Impact

We will construct a simple two-layer monad tranformers stack using StateT and ListT on top of IO to fulfill some functionalities for demonstration.

Task Description

Summing up numbers in a stream

The stream will be abstracted as a list of Integers, so our ListT comes in. To sum them up, we need to keep a state of the sum while processing each item in the stream, where our StateT comes.

Two Stacks

We have a simple state as Int to keep the sum

  • ListT (StateT Int IO) a
  • StateT Int (ListT IO) a

Full Program

#!/usr/bin/env stack
-- stack script --resolver lts-11.14 --package list-t --package transformers

import ListT (ListT, traverse_, fromFoldable)
import Control.Monad.Trans.Class (lift)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.State (StateT, evalStateT, get, modify)

main :: IO()
main =  putStrLn "#### Task: summing up numbers in a stream"
     >> putStrLn "####       stateful (StateT) stream (ListT) processing"
     >> putStrLn "#### StateT at the base: expected result"
     >> ltst
     >> putStrLn "#### ListT at the base: broken states"
     >> stlt



-- (ListT (StateT IO)) stack
ltst :: IO ()
ltst = evalStateT (traverse_ (\_ -> return ()) ltstOps) 10

ltstOps :: ListT (StateT Int IO) ()
ltstOps = genLTST >>= processLTST >>= printLTST

genLTST :: ListT (StateT Int IO) Int
genLTST = fromFoldable [6,7,8]

processLTST :: Int -> ListT (StateT Int IO) Int
processLTST x = do
    liftIO $ putStrLn "process iteration LTST"
    lift $ modify (+x)
    lift get

printLTST :: Int -> ListT (StateT Int IO) ()
printLTST = liftIO . print



-- (StateT (ListT IO)) stack
stlt :: IO ()
stlt = traverse_ (\_ -> return ())
     $ evalStateT (genSTLT >>= processSTLT >>= printSTLT) 10

genSTLT :: StateT Int (ListT IO) Int
genSTLT = lift $ fromFoldable [6,7,8]

processSTLT :: Int -> StateT Int (ListT IO) Int
processSTLT x = do
    liftIO $ putStrLn "process iteration STLT"
    modify (+x)
    get

printSTLT :: Int -> StateT Int (ListT IO) ()
printSTLT = liftIO . print

Results And Explanation

$ ./order.hs   
#### Task: summing up numbers in a stream
####       stateful (StateT) stream (ListT) processing
#### StateT at the base: expected result
process iteration LTST
16
process iteration LTST
23
process iteration LTST
31
#### ListT at the base: broken states
process iteration STLT
16
process iteration STLT
17
process iteration STLT
18

The first stack ListT (StateT Int IO) a yields the correct result since StateT is evaluated after ListT. When evaluating StateT, the runtime system already evaluated all operations of ListT -- feeding the stack with a stream [6,7,8], going through them with traverse_. The word evaluated here means effects of ListT are gone and ListT is transparent to StateT now.

The second stack StateT Int (ListT IO) a does not have the correct result since StateT is too short-lived. In every iteration of ListT evaluation, a.k.a., traverse_, the state is created, evaluated and vanished. The StateT in this stack structure does not achieve its purpose to keep states between list/stream item operations.



来源:https://stackoverflow.com/questions/5075621/what-is-the-difference-between-different-orderings-of-the-same-monad-transformer

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!