TransLecture (State 9)

Monads as effects

You can look at monads as at ways to represent effects.

Monad Effect
Maybe Computation can fail.
Either Computation can fail with annotated error.
[] Computation has multiple values.
Writer Computation has monoidal accumulator.
Reader Computation has access to some immutable context.
State Computation has access to some mutable state.
IO Computation can perform I/O actions.

We want to write functions which 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

Want to compose 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!

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

Redundant Writer

Other monads

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 safe

Now 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!

Why and how?? It's a long story....

Why it's a problem at all?

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

Functors and Applicatives compose. Monads — don't.

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

English

Haskell

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

But not for Monad! :(

-- you can't implement such instance :(((((

instance (Monad f, Monad g) => Monad (Compose f g) 

Composition of two monads can't be a Monad automatically.

Thus we need to consider each monad separately...

Maybe

Good old 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
          ...

Combine 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"
    c2 <- MaybeIO $ tryConnect "host2"

Nice

How about this?

Transforming IO into MaybeIO

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

MaybeT transformer

newtype MaybeIO a = MaybeIO
    { runMaybeIO :: IO (Maybe a) }
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) }
transformToMaybeT :: Functor m => m a -> MaybeT m a
transformToMaybeT = MaybeT . fmap Just

General version of transform

class MonadTrans t where    -- t :: (* -> *) -> * -> *
    lift :: Monad m => m a -> t m a
    
    {-# LAWS

        1. lift . return  ≡ return
        2. lift (m >>= f) ≡ lift m >>= (lift . f)

    #-}
transformToMaybeT  :: Monad m => m a -> MaybeT    m a
transformToEitherT :: Monad m => m a -> EitherT l m a
instance MonadTrans MaybeT where
    lift :: Monad m => m a -> MaybeT m a
    lift = transformToMaybeT

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 $ maybe (runMaybeT y) pure $ runMaybeT x

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 a = ReaderT LoggerName IO a

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 }

ReaderT transformer example

ReaderT monad

newtype ReaderT r m a = ReaderT 
    { runReaderT :: r -> m a }
type Reader r a 
  = ReaderT r          Identity a
type LoggerIO a 
  = ReaderT LoggerName IO       a
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

Base monad Transformer Original type Combined type
Maybe MaybeT Maybe a m (Maybe a)
Either EitherT 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 IO transformer in table.

Where is IOT?

IO can't be a transformer

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 functions for Reader
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

Parser Combinators

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

type Parser a = StateT String Maybe a

Reminder of our parser combinators type

It's just a special case of other transformers combination!

Exceptions with MonadThrow

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 MonadTrans type class and classic monad types (ReaderT, StateT, etc.)

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

Cost: n * m instances problem.

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

How to easily convert to mtl

-- Complex type for which we need to write all instances manually :(

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

-- Move all dirty work to 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 pseudo syntax

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

Function `foo` above 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 IO ()

bar :: StateT IO ()

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

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

-- Fragile code, don't write so!
foobar :: ExceptT Err (Except Err) ()

Mind order!

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

StateT is hard to safely handle with IO exceptions, 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 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 example)

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

More expressive, hah?

 

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 exactly 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,319