简体   繁体   中英

Confused about Haskell Monad Transformers

I am confused about where m should be placed on the right side of the Monad transformers?

For example:

WriterT is defined as

newtype WriterT w m a = WriterT { runWriterT :: m (a, w) }

while ReaderT is defined as

newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }

but NOT

newtype ReaderT r m a = ReaderT { runReaderT :: m (r -> a) }

The placement of the monad m will depend on the function and operation of the monad transformer that's being applied to the underlying monad m , so it's determined by what functionality the reader and writer are supposed to be adding to the monad.

It helps to remember that runReaderT and runWriterT aren't really doing anything, despite their suggestive names. They're just unwrapping a newtype, and it's the things they wrap that are transforming the monad m .

What I mean by this is, given a monad m , you can add a reader to it by considering monadic actions of type:

r -> m a

and you can add a writer to it by considering monadic actions of type:

m (a, w)

and you can add a reader, writer, and state to it by considering monadic actions of type:

r -> s -> m (a, s, w)

(That is, you don't need any of the transformer wrappers to do this, though they can make it more convenient, particularly since you can use existing operators like >>= and <*> instead of having to define your own.)

So, when you add a reader to a monad m , why don't you instead place the m at the beginning and consider monadic actions of following type?

m (r -> a)

You could, in fact, do this, but you'd quickly discover that this method of adding a reader doesn't actually add very much functionality to the monad m .

For example, suppose you're writing a function that should look up a key in a table of values, and you want to carry the table in a reader. Since the lookup can fail, you'd like to do this in the Maybe monad. So, you'd like to write something like:

myLookup :: Key -> Maybe Value
myLookup key = ...

However, you want to enhance the Maybe monad with a reader that provides the table of keys and values. If we do this using the m (r -> a) pattern, we get:

myLookup :: Key -> Maybe ([(Key,Value)] -> Value)

Now, let's try to implement it:

myLookup k = Just (\tbl -> ...)

Already, we see a problem. We have to provide a Just (indicating that the lookup has succeeded) before we're allowed to write code to access the \\tbl . That is, the monadic action (failure or success with return value) cannot depend on information in the r which should have been obvious from the signature m (r -> a) . Using the alternate r -> ma pattern is more powerful:

type M a = ([Key,Value]) -> Maybe a
myLookup :: Key -> M Value
myLookup key tbl = Prelude.lookup key tbl

@Thomas_M_DuBuisson gave another example. If we're trying to read an input file, we might write:

readInput :: FilePath -> IO DataToProcess
readInput fp = withFile fp ReadMode $ \h -> ...

It would be nice to carry around configuration information like file paths in a reader, so let's transform it using the pattern m (r -> a) to:

data Config = Config { inputFile :: FilePath }
readConfig :: IO (Config -> DataToProcess)
readConfig = ...um...

and we're stuck because we can't write an IO action that depends on the configuration information. If we'd used the alternate pattern r -> ma , we'd be set:

type M a = Config -> IO a
readConfig :: M DataToProcess
readConfig cfg = withFile (inputFile cfg) ReadMode $ ...

Another issue, raised by @cdk, is that this new "monadic" action type:

m (r -> a)

isn't even a monad. It's weaker (just an applicative).

Note that adding a merely applicative reader to a monad could still be useful. It just needs to be used in computations where the computational structure does not depend on the information in r . (So, if the underlying monad is Maybe to allow a computation to signal an error, the values from r can be used in the computation but the determination of whether or not the computation succeeds must be independent of r .)

However, the r -> ma version is strictly more powerful and can be used as both a monadic and applicative reader.

Note that some monadic transformations are useful in multiple forms. For example, you can (but only sometimes, as @luqui pointed out in a comment) add a writer to an m monad in two ways:

m (a, w)  -- if m is a monad this is always a monad
(m a, w)  -- this is a monad for some, but not all, monads m

If m is IO , then IO (a,w) is way more useful than (IO a, w) -- with the latter, the written w (eg, an error log) can't depend on the result of executing the IO action! Also, again (IO a, w) isn't actually a monad; it's just an applicative.

On the other hand, if m is Maybe , then (Maybe a, w) writes something whether the computation succeeds or fails, while Maybe (a, w) loses all the log entries if it returns Nothing . Both forms are monads and can be useful in different situations, and they correspond to stacking the transformers in different orders:

MaybeT (Writer w)  -- acts like  (Maybe a, w)
WriterT w Maybe    -- acts like  Maybe (a, w)

The same is not true for stacking Maybe and Reader in different orders. Both of these are isomorphic to the "good" reader r -> Maybe a :

MaybeT (Reader r)
ReaderT r Maybe

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM