Часто нужна монада, которая одновременно, например, Reader и Writer. Для этого используются трансформаторы монад. Сначала более общая идея: композиция типов.
Рассмотрим функции
Проводя аналогию между значениями и типами, запишем аналогичные определения для конструкторов типов.
Начнём с id
.
Haskell использует понятие “род” (kind) для классификации типов.
Обычные типы имеют род Type
или *
(синонимы).
Конструкторы одного аргумента – род Type -> Type
или * -> *
Конструкторы двух аргументов – род Type -> Type -> Type
или * -> * -> *
И т.д.
В GHCi вывести род типа – команда :kind Typename
.
Identity
имеет род Type -> Type
– конструктор типа одного аргумента.
Identity
является (скучной) монадой:
Аналог оператора (.)
на уровне типов имеет род
Для сравнения
Из рода, вывод – конструктор трёх аргументов, два из которых – конструкторы одного аргумента, третий – обычный:
На практике может выглядеть так:
> let x = [Just (1::Int), Nothing]
> let y = Compose x
> :t x
x :: [Maybe Int]
> :t y
y :: Compose [] Maybe Int
Композиция – Compose [] Maybe
. Применяется к Int
, в результате – [Maybe Int]
.
Для Compose
сравнительно легко получаются экземпляры Functor
и Applicative
:
instance (Functor f, Functor g)
=> Functor (Compose f g) where
fmap f (Compose x) = Compose $ fmap (fmap f) x
instance (Applicative f, Applicative g)
=> Applicative (Compose f g) where
pure x = Compose $ pure (pure x)
(Compose f) <*> (Compose x)
= Compose $ fmap (<*>) f <*> x
Этого достаточно для многих практически важных случаев.
В случае с Monad
нас ожидает трудность:
Не будем останавливаться на доказательстве, лишь отметим, что использованный выше подход для функторов здесь не работает.
Проблема – композицию монад можно записать, но она – не монада.
Решение – трансформатор монад. Трансформатор монад – конструктор типа, принимающий монаду и возвращающий монаду.
Основная проблема композиции монад – нехватка информации. Если фиксировать одну монаду в композиции, получим трансформатор. Т.е., для каждой монады существует свой трансформатор.
Рассмотрим Identity:
Трансформатор – IdentityT
:
Добавили аргумент m :: Type -> Type
.
Попробуем описать монаду для IdentityT
:
instance Functor m => Functor (IdentityT m) where
fmap f (IdentityT x) = IdentityT $ fmap (fmap f) x
instance Applicative m
=> Applicative (IdentityT m) where
pure x = IdentityT $ pure (pure x)
(IdentityT f) <*> (IdentityT x)
= IdentityT $ fmap (<*>) f <*> x
instance Monad m => Monad (IdentityT m) where
(IdentityT x) >>= f
= IdentityT $ fmap runIdentity x
>>= runIdentityT . f
Ключевой момент: зная, как “выполнить” Identity
, можем “достать” вторую, сделать, что требуется, “завернуть” обратно. В Compose
такой возможности нет, т.к. ни одна монада не фиксирована.
MaybeT
instance Functor m => Functor (MaybeT m) where
fmap f (MaybeT ma) =
MaybeT $ (fmap . fmap) f ma
instance Applicative m => Applicative (MaybeT m) where
pure = MaybeT . pure . pure
(MaybeT f) <*> (MaybeT x)
= MaybeT $ fmap (<*>) f <*> x
Functor
и Applicative
неинтересные, повторяют Compose
.
Monad
аналогично Maybe
:
instance Monad m => Monad (MaybeT m) where
(MaybeT x) >>= f
= MaybeT $
x >>= maybe (pure Nothing)
(runMaybeT . f)
Через do-нотацию:
instance Monad m => Monad (MaybeT m) where
(MaybeT x) >>= f = MaybeT $ do
ma <- x
case ma of
Nothing -> pure Nothing
Just x -> runMaybeT (f x)
Для сравнения:
EitherT
newtype EitherT e m a =
EitherT { runEitherT :: m (Either e a) }
instance Monad m => Monad (EitherT e m) where
(EitherT x) >>= f
= EitherT $
x >>= either (pure . Left)
(runEitherT . f)
Functor
и Applicative
– те же. Здесь и далее опускаем.
Через do-нотацию:
instance Monad m => Monad (EitherT e m) where
(EitherT x) >>= f = EitherT $ do
ea <- x
case ea of
Left e -> pure (Left e)
Right x -> runEitherT (f x)
Для сравнения:
Вообще, EitherT
в библиотеке называется ExceptT
ReaderT
newtype ReaderT r m a =
ReaderT { runReaderT :: r -> m a }
instance Monad m => Monad (ReaderT r m) where
(ReaderT x) >>= f
= ReaderT $ \r -> do
y <- x r
runReaderT (f y) r
Для сравнения:
StateT
newtype StateT s m a =
StateT { runStateT :: s -> m (a, s) }
instance Monad m => Monad (StateT s m) where
x >>= f
= StateT $ \s -> do
(a, s') <- runStateT x s
runStateT (f a) s'
Для сравнения:
На самом деле, для монад Reader
, State
и т.п. “простые” монады определяются не явно, а в терминах трансформаторов.
Например, Reader
можно легко получить из ReaderT
, применив его к Identity
:
Чтобы “вычислить”, выполнить все трансформаторы, начиная с внешнего:
mval :: EitherT String (
MaybeT (
ReaderT () Identity
)
) Int
mval = return 0
val = runIdentity (
runReaderT (
runMaybeT (runEitherT mval)
) ())
Из-за порядка применения функций – синтаксически вычисление “внешнего” трансформатора в типе оказывается “внутренним” выражением.
Какой тип у val
?
Значение имеет монаду внешнего трансформатора на внутренней позиции. Связано с порядком вычисления и с необходимостью иметь монаду на внешнем уровне вложенности после “выполнения” каждого трансформатора (если бы это было не так, мы не могли бы последовательно применять функции runSomethingT
).
Мы не рассматривали трансформаторы для монад списков и Writer
. Они – сложные и редко используемые.
Практически, WriterT
, как и Writer
подходит только для достаточно специфических применений. Не слишком хорошо подходит для логгирования, несмотря на провокационное название.
Наивная реализация ListT
(доступная в библиотеке transformers
) почти никогда не удовлетворяет закону ассоциативности ⇒ приводит к крайне неожиданным результатам.
Корректная реализация (доступная в библиотеке list-t) – достаточно сложная и медленная.
На практике используются библиотеки, работающие с “потоками”: streaming
, conduit
, pipes
, streamly
.
Трансформаторы монад – в классе MonadTrans
:
Функция lift
“поднимает” операцию вложенной монады на один уровень стека трансформаторов.
Например,
import Control.Monad.Except
safeIntegerDivision :: Int -> Int -> Maybe Int
safeIntegerDivision a b
| b == 0 = Nothing
| otherwise = Just (a `div` b)
doStuff :: Int -> Int -> ExceptT String Maybe Int
doStuff a b = do
val <- lift $ safeIntegerDivision a b
when (val == 0) $ throwError "Value is zero!"
return val
safeIntegerDivision
возвращает Maybe Int
, но с lift
используется в контексте ExceptT String Maybe Int
.
lift
“поднимает” один уровень стека. Похоже на pure
, вместо “чистых” значений – значения в монадах.
Реализация MonadTrans
– достаточно простая, например
Проблема с lift
: наивное использование приводит к
Решение – новый тип трансформатора, абстрагирующий стек трансформаторов:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.Trans.Maybe
newtype ReaderExceptMaybeT r e m a
= ReaderExceptMaybeT {
runREMT :: ReaderT r (ExceptT e (MaybeT m)) a
} deriving ( Functor, Applicative, Monad
, MonadReader r, MonadError e)
instance MonadTrans (ReaderExceptMaybeT r e) where
lift = ReaderExceptMaybeT . lift . lift . lift
При необходимости также:
С другого конца – класс, MonadIO
. “Поднимает” действия в IO
через весь стек:
Экземпляр при наличии экземпляра MonadTrans
: