Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Moving StateT into and out of IO

Tags:

haskell

monads

I'm sure I must be missing something.

I'm new to Haskell and the curve is very steep. I'm reaching the point in my toy project where I really want to use a State monad to avoid passing a thousand arguments everywhere. I'm having trouble understanding how to pass that State monad from IO into pure code. Something conceptually like this (except with StateT instead of ExceptT):

import Control.Monad.Except
import Control.Monad.Identity

type PlayM = Except String 
type PlayMIO = ExceptT String IO

puree :: String -> PlayM String
puree = return . ("bb"++)

impuree :: String -> PlayMIO String
impuree s = do
  a <- return $ runIdentity $ runExceptT $ puree s
  return $ "aa" ++ a

main = do
  runExceptT $ impuree "foo"
  putStrLn "hi"

except this doesn't compile, giving me something like this:

play.hs:15:20:
Couldn't match expected type ‘[Char]’
            with actual type ‘Either String String’
In the second argument of ‘(++)’, namely ‘a’
In the second argument of ‘($)’, namely ‘"aa" ++ a’

I understand now why this doesn't compile and why the types are what they are, but for the life of me, I cannot understand how to do this. This feels like it shouldn't be hard, but my intuition in Haskell is far from accurate.

Thanks for your help!

-g

like image 311
George Madrid Avatar asked Mar 15 '23 00:03

George Madrid


2 Answers

You're close. Let's follow the types with type holes (_s):

impuree :: String -> PlayMIO String
impuree s = do
  a <- _ . runIdentity . runExceptT $ puree s
  return $ "aa" ++ a

This tells us we need a type:

Test.hs:15:8:
    Found hole ‘_’
      with type: m0 (Either String String) -> ExceptT String IO [Char]
    Where: ‘m0’ is an ambiguous type variable
    Relevant bindings include
      s :: String (bound at Test.hs:13:9)
      impuree :: String -> PlayMIO String (bound at Test.hs:13:1)
    In the first argument of ‘(.)’, namely ‘_’
    In the expression: _ . return . runIdentity . runExceptT
    In a stmt of a 'do' block:
      a <- _ . return . runIdentity . runExceptT $ puree s

Now, we have something that can turn a m (Either e b) into an ExceptT e m b:

ExceptT :: m (Either e b) -> ExceptT e m b

Applying that, we get the correct answer:

impuree :: String -> PlayMIO String
impuree s = do
  a <- ExceptT . return . runIdentity . runExceptT $ puree s
  return $ "aa" ++ a

If we look at the docs, we can see that the pattern ExceptT . f . runExceptT is abstracted with the function

mapExceptT :: (m (Either e a) -> n (Either e' b)) -> ExceptT e m a -> ExceptT e' n b

In our case, the m is Identity and the n is IO. Using this, we get:

impuree :: String -> PlayMIO String
impuree s = do
  a <- mapExceptT (return . runIdentity) $ puree s
  return $ "aa" ++ a

Abstracting the pattern

This would probably be overkill here, but it is worth noting that there is a package called mmorph which makes it nicer to work with monad morphisms (transformations from one monad to another). This package has a function generalize :: Monad m => Identity a -> m a which we can use:

impuree :: String -> PlayMIO String
impuree s = do
  a <- mapExceptT generalize $ puree s
  return $ "aa" ++ a

A further generalization

While we're talking about mmorph, we may as well use the more general form:

impuree :: String -> PlayMIO String
impuree s = do
  a <- hoist generalize $ puree s
  return $ "aa" ++ a

hoist generalizes mapExceptT to any monad transformer-like thing where you can apply a monad morphism to the underlying monad:

hoist :: (MFunctor t, Monad m) => (forall a. m a -> n a) -> t m b -> t n b

Everything after the first correct answer here is just bonus stuff and it isn't necessary to understand it in order to understand and use the solution. It could come in handy at some point though, which is why I included it. Recognizing the general pattern of monad morphisms saves time, but you can always do things more explicitly without that additional level of abstraction.

like image 101
David Young Avatar answered Mar 23 '23 21:03

David Young


An alternate approach is to simply make your puree and impuree operations type-class polymorphic. This is the usual mtl way: require some classes, then somewhere at the top-level pick a concrete monad that instantiates all the appropriate classes. Thus:

import Control.Monad.Except
import Control.Monad.Identity

type PlayM = Except String 
type PlayMIO = ExceptT String IO

puree :: Monad m => String -> m String
puree = return . ("bb"++)

impuree :: Monad m => String -> m String
impuree s = do
  a <- puree s
  return $ "aa" ++ a

main = do
  runExceptT $ impuree "foo"
  putStrLn "hi"

In this example, your code is especially uninteresting, because you didn't use any of the special powers of IO or ExceptT. Here's how it might look if you had:

-- in particular, puree :: String -> PlayM String
puree :: MonadError String m => String -> m String
puree "heck" = throwError "Watch your language!"
puree s = return ("bb" ++ s)

-- in particular, impuree :: String -> PlayMIO String
impuree :: (MonadError String m, MonadIO m) => String -> m String
impuree s = do
  s' <- puree s
  liftIO . putStrLn $ "hah! what kind of input is " ++ s ++ "?!"
  return ("aa" ++ s)

main = do
  runExceptT (impuree "foo")
  putStrLn "hi"
like image 29
Daniel Wagner Avatar answered Mar 23 '23 21:03

Daniel Wagner