ITMO CTD Haskell
Lecture slides on Functional programming course at the ITMO university CT department. You can find course description here: https://github.com/jagajaga/FP-Course-ITMO
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.
Let's look at Reader (usually effects can be represented as functions)
foo :: String -> Env -> Int
foo :: String -> Reader Env Int
Simple for beginners
Effect is handled automatically
foo :: UnknownType
foo i = do
baseCounter <- ask
let newCounter = baseCounter + i
put [baseCounter, newCounter]
return newCounter
foo :: RWS Int [Int] () Int
foo i = do
baseCounter <- ask
let newCounter = baseCounter + i
put [baseCounter, newCounter]
return newCounter
foo :: State (Int, [Int]) Int
foo i = do
x <- gets fst
let xi = x + i
put (x, [x, xi])
return xi
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
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.
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.
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
...
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?
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"
...
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
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
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
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 }
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
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 |
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
IO can't be a transformer
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
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
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!
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
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
This helps to avoid writing lift manually...
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.
-- 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
foo :: ( MonadState MyState m
, MonadReader Environment m
, MonadIO m
) => Text -> m Text
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]
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
Unless you write a library, just use the concrete monad!
When you want to test your application code in pure way.
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) ()
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.
There are a lot of custom defined Monad* classes in mtl style
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.
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 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.
-- | 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...
By ITMO CTD Haskell
Lecture about monad transformers.
Lecture slides on Functional programming course at the ITMO university CT department. You can find course description here: https://github.com/jagajaga/FP-Course-ITMO