TransLecture (State 7)

Monads as effects

You can look at monads as ways to represent effects.

Monad Effect
Maybe Computation can fail (store 0 or 1 values).
Either Computation can fail with an annotated error.
[] Computation stores 0 or more values.
Writer Computation has a monoidal logging accumulator.
Reader Computation has access to some immutable environment.
State Computation is stateful.
IO Computation can perform I/O actions.

We want to write functions that have access to multiple effects. ⇒ We want to compose effects.

Translation of effects

Let's look at Reader (usually effects can be represented as functions).

foo :: String -> Env -> Int

Fun

foo :: String -> Reader Env Int

Effect

Simple for beginners

Effect is handled automatically

Composing Reader and State

foo :: UnknownType
foo i = do
    baseCounter <- ask
    let newCounter = baseCounter + i
    put [baseCounter, newCounter]
    return newCounter

Reader has ask but doesn't have put

State has put but doesn't have ask

RWS?

newtype RWS r w s a = RWS
    { runRWS :: r -> s -> (a, s, w) }

foo :: RWS Int () [Int] Int
foo i = do
    baseCounter <- ask
    let newCounter = baseCounter + i
    put [baseCounter, newCounter]
    return newCounter

Redundant Writer

Only State?

foo :: State (Int, [Int]) Int
foo i = do
    x <- gets fst
    let xi = x + i
    put (x, [x, xi])
    return xi

Not enough type-safety

Enter: ReaderT and StateT

foo :: Int -> ReaderT Int (State [Int]) Int  -- or StateT [Int] (Reader Int) Int
foo i = do
    baseCounter <- ask
    let newCounter = baseCounter + i
    put [baseCounter, newCounter]
    return newCounter

The transformers!

Let us begin.

Why is it a problem at all?

newtype (g |.| f) a = Compose { getCompose :: g (f a) }

Functors and Applicatives compose. Monads, in general — don't.

If f is a Functor and g is a Functor, then composition of g and f is also a Functor (i.e. g |.| f is a Functor). Same for Applicative, Alternative, Foldable, Traversable.

In English:

In Haskell:

instance (Functor     f, Functor     g) => Functor     (g |.| f)
instance (Foldable    f, Foldable    g) => Foldable    (g |.| f)
instance (Traversable f, Traversable g) => Traversable (g |.| f)
instance (Applicative f, Applicative g) => Applicative (g |.| f)
instance (Alternative f, Alternative g) => Alternative (g |.| f)

But not for Monad

instance (Monad f, Monad g) => Monad (g |.| f) -- impossible in general

Composition of two Monads is not always a Monad.

Thus we need to consider each monad separately.

Maybe

Good ole plain Haskell

tryConnect :: HostName -> IO (Maybe Connection)

foo :: IO (Maybe smth)
foo = do
    mc1 <- tryConnect "host1"
    case mc1 of
        Nothing -> return Nothing
        Just c1 -> do
            mc2 <- tryConnect "host2"
            case mc2 of
                Nothing -> return Nothing
                Just c2 -> do
                    ...

Combining Maybe and IO

newtype MaybeIO a = MaybeIO { runMaybeIO :: IO (Maybe a) }
instance Monad MaybeIO where
    return x = MaybeIO (return (Just x))
    MaybeIO action >>= f = MaybeIO $ do
        result <- action
        case result of
            Nothing -> return Nothing
            Just x  -> runMaybeIO (f x)
result <- runMaybeIO $ do
    c1 <- MaybeIO $ tryConnect "host1"
    c2 <- MaybeIO $ tryConnect "host2"
    ...
result <- runMaybeIO $ do
    c1 <- MaybeIO $ tryConnect "host1"
    print "Hello" -- typechecking fails, but what if we REALLY want to do this?
    c2 <- MaybeIO $ tryConnect "host2"

Nice

But what about this?

Lifting IO into MaybeIO

liftIOToMaybeIO :: IO a -> MaybeIO a
liftIOToMaybeIO action = MaybeIO $ do
    result <- action
    return (Just result)
result <- runMaybeIO $ do
  c1 <- MaybeIO $ tryConnect "host1"
  liftIOToMaybeIO $ print "Hello"
  c2 <- MaybeIO $ tryConnect "host2"
  ...

Charming

liftIOToMaybeIO :: IO a -> MaybeIO a
liftIOToMaybeIO action = MaybeIO $ Just <$> action

The MaybeT transformer

type MaybeIO = MaybeT IO
instance Monad m => Monad (MaybeT m) where
    return :: a -> MaybeT m a
    return x = MaybeT (return (Just x))

    (>>=) :: MaybeT m a -> (a -> MaybeT m b) -> MaybeT m b
    MaybeT action >>= f = MaybeT $ do
        result <- action
        case result of
            Nothing -> return Nothing
            Just x  -> runMaybeT (f x)
newtype MaybeT m a = MaybeT 
    { runMaybeT :: m (Maybe a) }
liftToMaybeT :: Functor m => m a -> MaybeT m a
liftToMaybeT = MaybeT . fmap Just

Lifting: a fundamental operation

class MonadTrans t where   -- t :: (Type -> Type) -> Type -> Type
                           -- all monad transformers have exactly this type
                           -- all monad transformers instantiate MonadTrans
    lift :: Monad m => m a -> t m a
liftToMaybeT  :: Monad m => m a -> MaybeT    m a
liftToExceptT :: Monad m => m a -> ExceptT e m a
instance MonadTrans MaybeT where
    lift :: Monad m => m a -> MaybeT m a
    lift = liftToMaybeT
1. lift . return  ≡ return
2. lift (m >>= f) ≡ lift m >>= (lift . f)
3. lift . join = join . lift . fmap lift   -- redundant, but still valid

MonadTrans laws

Using MaybeT in real life

emailIsValid :: String -> Bool
emailIsValid email = '@' `elem` email

askEmail :: IO (Maybe String)
askEmail = do
    putStrLn "Input your email, please:"
    email <- getLine
    return $ if emailIsValid email
             then Just email 
             else Nothing 
main :: IO ()
main = do
    email <- askEmail
    case email of
        Nothing     -> putStrLn "Wrong email."
        Just email' -> putStrLn $ "OK, your email is " ++ email'
emailIsValid :: String -> Bool
emailIsValid email = '@' `elem` email

askEmail :: MaybeT IO String
askEmail = do
    lift $ putStrLn "Input your email, please:"
    email <- lift getLine
    guard $ emailIsValid email
    return email
main :: IO ()
main = do
    email <- runMaybeT askEmail
    case email of
        Nothing     -> putStrLn "Wrong email."
        Just email' -> putStrLn $ "OK, your email is " ++ email'
main :: IO ()
main = do
    Just email <- runMaybeT $ untilSuccess askEmail
    putStrLn $ "OK, your email is " ++ email

untilSuccess :: Alternative f => f a -> f a
untilSuccess = foldr (<|>) empty . repeat

-- Defined in Control.Monad.Trans.Maybe
instance (Functor m, Monad m) => Alternative (MaybeT m) where
    empty = MaybeT (return Nothing)
    x <|> y = MaybeT $ runMaybeT x >>= maybe (runMaybeT y) (return . Just)

Reader

Environment + IO

newtype LoggerName = LoggerName { getLoggerName :: Text }

logMessage :: LoggerName -> Text -> IO ()
readFileWithLog :: LoggerName -> FilePath -> IO Text
readFileWithLog loggerName path = do
    logMessage loggerName $ "Reading file: " <> T.pack (show path)
    readFile path
main :: IO ()
main = prettifyFileContent (LoggerName "Application") "foo.txt"
writeFileWithLog :: LoggerName -> FilePath -> Text -> IO ()
writeFileWithLog loggerName path content = do
    logMessage loggerName $ "Writing to file: " <> T.pack (show path)
    writeFile path content
prettifyFileContent :: LoggerName -> FilePath -> IO ()
prettifyFileContent loggerName path = do
    content <- readFileWithLog loggerName path
    writeFileWithLog loggerName path (format content)
type LoggerIO = ReaderT LoggerName IO

logMessage :: Text -> LoggerIO ()
readFileWithLog :: FilePath -> LoggerIO Text
readFileWithLog path = do
    logMessage $ "Reading file: " <> T.pack (show path)
    lift $ readFile path
main :: IO ()
main = runReaderT (prettifyFileContent "foo.txt") (LoggerName "Application") 
writeFileWithLog :: FilePath -> Text -> LoggerIO ()
writeFileWithLog path content = do
    logMessage $ "Writing to file: " <> T.pack (show path)
    lift $ writeFile path content
prettifyFileContent :: FilePath -> LoggerIO ()
prettifyFileContent path = do
    content <- readFileWithLog path
    writeFileWithLog path (format content)
newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }

The ReaderT transformer

ReaderT: Monad(Trans) instance

newtype ReaderT r m a = ReaderT 
    { runReaderT :: r -> m a }
type Reader r 
  = ReaderT r          Identity
type LoggerIO 
  = ReaderT LoggerName IO
instance Monad m => Monad (ReaderT r m) where
    return  = lift . return
    m >>= f = ReaderT $ \r -> do
        a <- runReaderT m r
        runReaderT (f a) r
instance MonadTrans (ReaderT r) where
    lift :: m a -> ReaderT r m a
    lift = ReaderT . const
 -- lift ma = ReaderT $ \_ -> ma

Transformers table

Precursor Transformer Original type Combined type
Maybe MaybeT Maybe a m (Maybe a)
Either ExceptT Either a b m (Either a b)
Writer WriterT (a, w) m (a, w)
Reader ReaderT r -> a r -> m a
State StateT s -> (a, s) s -> m (a, s)
Cont ContT (a -> r) -> r (a -> m r) -> m r

Special class for IO

class Monad m => MonadIO m where
    liftIO :: IO a -> m a
instance MonadIO IO where
    liftIO = id
instance MonadIO m => MonadIO (StateT s m) where
    liftIO = lift . liftIO
instance MonadIO m => MonadIO (ReaderT r m) where
    liftIO = lift . liftIO

Hey, I don't see the IO transformer in the table.

Where is IOT?

IO cannot be a transformer

etc.

Try writing a Monad instance for IOT and deduce the issue yourself!

Hey, what about composing?

foo :: Int -> StateT [Int] (Reader Int) Int
foo i = do
    baseCounter <- lift ask
    let newCounter = baseCounter + i
    put [baseCounter, newCounter]
    return newCounter
class Monad m => MonadReader r m | m -> r where
    ask    :: m r
    local  :: (r -> r) -> m a -> m a
    reader :: (r -> a) -> m a

mtl package

instance MonadReader r m => MonadReader r (StateT s m) where
    ask    = lift ask
    local  = mapStateT . local
    reader = lift . reader
-- good old simple implementation of all the Reader functions
instance Monad m => MonadReader r (ReaderT r m) where ...
foo :: Int -> StateT [Int] (Reader Int) Int
foo i = do
    baseCounter <- ask
    let newCounter = baseCounter + i
    put [baseCounter, newCounter]
    return newCounter

Connection with

parsers and parser combinators

newtype Parser a = Parser { runParser :: String -> Maybe (a, String) }
newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }

type Parser = StateT String Maybe

Recall our Parser type:

It's just a StateT specialization for more concrete types!

Exceptions with MonadThrow and MonadCatch

class Monad m => MonadThrow m where
    throwM :: Exception e => e -> m a
class MonadThrow m => MonadCatch m where
    catch :: Exception e => m a -> (e -> m a) -> m a
instance MonadThrow Maybe where
    throwM _ = Nothing

instance MonadThrow IO where
    throwM = Control.Exception.throwIO

instance MonadThrow m => MonadThrow (StateT s m) where
    throwM = lift . throwM
instance MonadCatch IO where
    catch = Control.Exception.catch

Throwing exceptions

Catching exceptions

Exceptions with MonadError

class Monad m => MonadError e m | m -> e where
    throwError :: e -> m a
    catchError :: m a -> (e -> m a) -> m a
foo :: MonadError FooError m => ...
bar :: MonadError BarError m => ...
baz :: MonadError BazError m => ...

data BazError = BazFoo FooError | BazBar BarError

baz = do
    withExcept BazFoo foo
    withExcept BazBar ba
newtype ExceptT e m a = ExceptT { runExceptT :: m (Either e a) }
runExceptT :: ExceptT e m a -> m (Either e a)
instance Monad m => MonadError e (ExceptT e m) where ...
withExceptT :: Functor m => (e -> e') -> ExceptT e m a -> ExceptT e' m a

mtl-style transformers

mtl package

This helps to avoid writing lift manually...

transformers package

Has only the MonadTrans typeclass and the classic monad transformer types (ReaderT, StateT, etc.)

Reexports transformers. For each monad SomeT adds a MultiParamTypeClass MonadSome with FunctionalDependencies.

Cost: the n * m instances problem.

If you have n monads and m typeclasses, you need to write n * m instances. But usually these instances are trivial one-liners.

How to easily convert to mtl

-- A complex type for which we need to write all the instances manually :(

newtype M a = M (Environment -> MyState -> IO (a, MyState))
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}

-- Leave all the dirty work to the compiler

newtype M a = M (ReaderT Environment (StateT MyState IO) a)
  deriving (Functor, Applicative, Monad, MonadIO, 
            MonadState MyState, MonadReader Environment)
foo :: Text -> M Text

Ugly mono style

Waterfall poly style

foo :: ( MonadState  MyState     m
       , MonadReader Environment m
       , MonadIO                 m
       ) => Text -> m Text

Constraints in Haskell

Some pseudocode

foo ::
  Context = m
  Effects m =
       Needs   Env
     , Updates Stack
     , Throws  EmptyStackError
     , Reads   "config/application.toml"
  Type = Int -> Int -> m [Int]
foo :: ( MonadReader Env m
       , MonadState  Stack m
       , MonadError  EmptyStackError m
       , MonadIO m
       )
    => Int -> Int -> m [Int]

Real Haskell code

The function foo is effectful and polymorphic.

Should one always write code like that?

Recommended patterns

ReaderT IO

data Env = Env
  { envServerAddress :: Text
  , envConn :: IORef (Maybe Conn)
  }

establishConn :: ReaderT Env IO ()
establishConn = do
  Env sAddr cRef <- ask
  prev <- liftIO $ readIORef cRef
  whenNothing prev $
    throwM ConnectionExists
  liftIO $
    connect sAddr >>=
    writeIORef cRef . Just

data AppError = ConnectionExists
  deriving Show
instance Exception AppError
data Name = Name String
data User = User { name :: Name
                 , age :: Int }

class Monad m => MonadDatabase m where
    getUser    :: Name -> m User
    deleteUser :: User -> m ()

test :: MonadDatabase m => m ()
test = do
  user <- getUser (Name "Pedro")
  when (age user < 18) (deleteUser user)

newtype AppM a = AppM (ReaderT Ctx IO a)
newtype TestM a = TestM (State [User] a)

main :: IO ()
main = runAppM test

Tagless final

Unless you write a library, just use the concrete monad!

When you want to test your application code in pure way.

Anti-patterns

Transformers + IO

foo :: ExceptT Err IO ()

bar :: StateT St IO ()

foobar :: ExceptT Err (StateT St IO) ()
-- Good way
foo :: ReaderT Ctx
        (ExceptT Err (State St)) ()

-- Little bit less efficient
bar :: ExceptT Err
        (StateT MyState (Reader Ctx)) ()

-- Fragile code!
foobar :: ExceptT Err (Except Err) ()

Mind the order!

ExceptT doesn't add anything to error handling in IO.

StateT is hard to safely handle with IO exceptions, the use of IORef should be preferred.

Be careful about the order of transformer composition.

With StateT over ExceptT wrong state might be restored after catch.

Why care about mtl and what's next?

There are a lot of custom defined Monad* classes in the mtl style

Other solutions

Monad transformers solve so-called Extensible effects problem. There exist different approaches for this problem, but monad transformers is the most popular and fastest approach.

Idris Effects (just an example)

eval : Expr -> Eff Integer [STDIO, EXCEPTION String, RND, STATE Env]
data Expr = Val Integer
          | Var String
          | Add Expr Expr
          | Random Integer

More expressive, huh?

 

Idris rulit' i razrulivaet?

Data type

Evaluation function

Idris-like effects in Haskell

data RandomGen = RandomGen
data WriteLn = WriteLn { unWriteLn :: String }

type instance EffectRes RandomGen = Int
type instance EffectRes WriteLn = ()

myExecution :: Free (MyF '[ RandomGen , WriteLn ]) Int
myExecution = do
  rand1 <- effect RandomGen
  rand2 <- effect RandomGen
  effect $ WriteLn $ "Generated two random numbers: "
    <> show rand1 <> " " <> show rand2
  pure $ rand1 ^ 2 + rand2

runExecution :: IO Int
runExecution = iterM (runMyF handlers) myExecution
  where
    handlers = Handler (const randomIO) :& Handler (putStrLn . unWriteLn) :& RNil

Haskell allows you to write essentially exactly the same code.

Full code uses free monads, DataKinds and other advanced machinery. To be continued on next lectures.

This reading will transform you

Coroutine (fun example)

-- | The CoroutineT monad is just ContT stacked with 
-- a StateT containing the suspended coroutines.
newtype CoroutineT r m a = CoroutineT 
    { runCoroutineT' :: ContT r (StateT [CoroutineT r m ()] m) a
    } deriving (Functor, Applicative, Monad, MonadCont, MonadIO)
printOne n = do
    liftIO (print n)
    yield

example = runCoroutineT $ do
    fork $ replicateM_ 3 (printOne 3)
    fork $ replicateM_ 4 (printOne 4)
    replicateM_ 2 (printOne 2)
3
4
3
2
4
3
2
4
4

...some implementation details ~50 lines...

Lecture 07: Transformers

By ITMO CTD Haskell

Lecture 07: Transformers

Lecture about monad transformers.

  • 6,682