Чтобы выработать некоторую интуицию касательно монад, полезно рассмотреть некоторые “стандартные” экземпляры этого класса, как они объявляются и как они используются.
Но сперва обсудим, что же именно описывает класс типов Monad
.
Для функций, возвращающих обычные значения, определена операция композиции, в Haskell обозначаемая оператором .
(“точка”):
(.) :: (b -> c) -> (a -> b) -> (a -> c)
. g) x = f (g x) (f
Отметим, что порядок применения функций – справа налево (сначала применяется g
, потом f
), что не всегда является удобной нотацией. В различных функциональных языках существует так же оператор, позволяющий указать применение функций слева направо. Haskell не исключение, в модуле Data.Function
объявлен оператор
(&) :: a -> (a -> b) -> b
& f = f x x
позволяющий записывать последовательное применение функций в виде
1 & (+1) & show & replicate 2 & concat
-- == (concat . replicate 2 . show . (+1)) 1
-- == concat (replicate 2 (show ((+1) 1)))
-- == "22"
Это удобно, но, увы, не работает для функций, которые возвращают значения в дополнительной структуре, например для частично-определённых на множестве действительных чисел функций
realSqrt :: Double -> Maybe Double
| x >= 0 = Just (sqrt x)
realSqrt x | otherwise = Nothing
realArcsin :: Double -> Maybe Double
| abs x <= 1 = Just (asin x)
realArcsin x | otherwise = Nothing
композиция вида realArcsin . realSqrt
невозможна: realSqrt
возвращает Maybe Double
, в то время как realArcsin
принимает просто Double
, без Maybe
. Однако кажется, что подобная композиция должна быть возможна: действительно, если realSqrt
возвращает Nothing
, то разумно сказать, что результат композиции так же не определён и присвоить ему Nothing
. Тогда такая композиция (обозначим её <=<
) будет иметь тип
(<=<) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
-- для сравнения, тип обычной композиции:
(.) :: (b -> c) -> (a -> b) -> (a -> c)
Можно также ввести аналог оператора &
, обозначим его >>=
:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
-- для сравнения
(&) :: a -> (a -> b) -> b
Теперь – абстрагируем. Maybe
не единственный тип, для которого эти операции могут быть осмыслены. Тогда обобщим сигнатуры до
(<=<) :: (b -> m c) -> (a -> m b) -> (a -> m c)
(>>=) :: m a -> (a -> m b) -> m b
Поскольку m
вообще говоря не любой конструктор типа – для каких-то ситуаций определить подобную композицию невозможно (либо есть более одного варианта, либо осмысленного варианта нет вообще), и поскольку конкретные определения будут зависеть от конкретных типов, для реализации этой абстракции вводится класс типов Monad
. Название взято из теории категорий, и математическую подоплёку оставим за скобками:
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
C практической точки зрения Monad
кодирует композицию (или что то же самое, последовательное применение) функций, возвращающих значение (или значения), “завёрнутое” в какую-то обобщённую структуру.
Отдельно отметим, что <=<
реализуется в терминах >>=
:
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
<=< g) = \x -> g x >>= f
(f
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
>=> f) = \x -> g x >>= f (g
Оператор <=<
и симметричный оператор >=>
с обратным порядком аргументов называются композицией Клейсли, а функции вида a -> m b
– стрелками Клейсли. Это понятия, опять, из теории категорий, и, опять, не будем на этом останавливаться.
Собственно законы монад, рассмотренные в предыдущей лекции, более просто выражаются через композицию Клейсли:
- Левая идентичность:
return >=> k ≡ k
- Правая идентичность:
m >=> return ≡ m
- Ассоциативность:
f >=> (k >=> h) ≡ (f >=> k) >=> h
По сути законы требуют, чтобы композиция Клейсли над стрелками Клейсли образовывала категорию.
Некоторые стандартные монады
Maybe
Тип Maybe
, позволяющий закодировать частично-определённую функцию в полностью определённой, является монадой. Напомним определение Maybe
:
data Maybe a = Nothing | Just a
Рассмотрим минимальное определение:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
instance Applicative Maybe where
pure = Just
Just f <*> m = fmap f m
Nothing <*> _m = Nothing
instance Monad Maybe where
Just x) >>= k = k x
(Nothing >>= _ = Nothing
Экземпляр Functor
описывает, как применяются функции, определённые над значениями, к значениям, “завёрнутым” в Maybe
: результатом значения Nothing
всегда является Nothing
, результатом значения Just x
– Just
результат применения функции к x
.
Экземпляр Applicative
описывает, в операторе <*>
, как фукнции, “завёрнутые” в Maybe
применяются к аргументам, “завёрнутым” в Maybe
. Кроме того, как значения могут быть “завёрнуты” в Maybe
в функции pure
.
Если вместо функции передано значение Nothing
, то результат применения такой “функции” к чему бы то ни было – Nothing
. В противном случае, <*>
по поведению совпадает с fmap
.
Наконец, экземпляр Monad
описывает (несколько окружным путём) композицию функций, возвращающих Maybe
: если первая функция вернула Nothing
, то нет смысла (и возможности) вычислять вторую, можно сразу вернуть Nothing
.
С прикладной точки зрения, монаду Maybe
удобно использовать для последовательностей вычислений (в широком смысле), которые на любом шаге могут закончиться неудачей. Типовый пример – поиск значения в какой-то базе данных:
import Data.List
newtype ItemId = ItemId Word deriving (Eq)
newtype CatId = CatId Word deriving (Eq)
type ItemName = String
type CategoryName = String
itemsTable :: [(ItemId, ItemName)]
= [ (ItemId 1, "Ноутбук")
itemsTable ItemId 2, "Клавиатура")
, (ItemId 3, "Трекбол")
, (ItemId 4, "Монитор")
, (ItemId 5, "Утюг")
, (ItemId 6, "Холодильник")
, (
]
categoriesTable :: [(CatId, CategoryName)]
= [ (CatId 1, "Компьютерная техника")
categoriesTable CatId 2, "Периферийные устройства")
, (CatId 3, "Манипуляторы")
, (CatId 4, "Бытовая техника")
, (
]
linkingTable :: [(ItemId, CatId)]
= [
linkingTable ItemId 1, CatId 1)
(ItemId 2, CatId 1)
, (ItemId 2, CatId 2)
, (ItemId 3, CatId 1)
, (ItemId 3, CatId 2)
, (ItemId 3, CatId 3)
, (ItemId 4, CatId 1)
, (ItemId 4, CatId 2)
, (ItemId 5, CatId 4)
, (ItemId 6, CatId 4)
, (
]
getCategoryForItem :: ItemName -> Maybe CategoryName
= do
getCategoryForItem searchName <- fst <$> find (\(key, value) -> value == searchName) itemsTable
itemId -- результат Nothing если в itemsTable нет элемента с именем searchName
<- lookup itemId linkingTable
categoryId -- результат Nothing если в linkingTable нет записей для данного itemId
lookup categoryId categoriesTable
-- финальный результат
здесь в роли “базы данных” выступают списки, но на практике это может быть, например, файл на диске или сервер SQL. Рассмотрим как работает функция getCategoryForItem
. Для этого перепишем do
-нотацию через >>=
:
=
getCategoryForItem searchName fst <$> find (\(key, value) -> value == searchName) itemsTable
>>= \itemId -> lookup itemId linkingTable
>>= \categoryId -> lookup categoryId categoriesTable
По порядку, find (\(key, value) -> value == searchName) itemsTable
находит первый элемент в списке itemsTable
, где второй элемент кортежа равен searchName
. Если значение найдено, то результат – Just (key, value)
, иначе Nothing
. fst <$>
использует оператор <$>
, эквивалентный fmap
. По определению fmap
для Maybe
, приведённому выше, fst
применяется к значению под Just
, либо результат – Nothing
, если аргумент – Nothing
. Вся первая строчка таким образом имеет значение Just key
или Nothing
.
Далее срабатывает первый оператор >>=
, по определению, если первая строчка – Nothing
, то результат всего выражения – Nothing
и вычисление прерывается. Иначе, значение под Just
передаётся в функцию \itemId -> ...
.
lookup itemId linkingTable
имеет значение Nothing
или Just catId
. Срабатывает второй оператор >>=
, в первом случае, результат всего выражения – Nothing
, во втором – значение catId
передаётся в функцию \categoryId -> ...
.
Наконец результатом всей конструкции является lookup categoryId categoriesTable
.
Таким образом, если какой-то из промежуточных шагов вычисления возвращает Nothing
, то вычисление прерывается и результат – Nothing
. В противном случае, значение под Just
передаётся дальше.
Either a
Конструктор типа Either
является конструктором двух параметров, поэтому сам по себе он не может быть функтором или монадой (эти классы типов определены для конструкторов типов одного параметра). Но конструкторы типов тоже можно применять частично, поэтому Either a
для любого типа a
является конструктором типа одного параметра, и, следовательно, для него можно реализовать экземпляр Functor
. Этот тип практически аналогичен Maybe
, но Nothing
может иметь дополнительное значение какого-то (фиксированного!) типа a
.
Напомним,
data Either a b = Left a | Right b
Рассмотрим определение:
instance Functor (Either a) where
fmap _ (Left x) = Left x
fmap f (Right y) = Right (f y)
instance Applicative (Either e) where
pure = Right
Left x <*> _ = Left x
Right f <*> r = fmap f r
instance Monad (Either e) where
Left x >>= _ = Left x
Right r >>= k = k r
Если сравнить с определением для Maybe
, то вся разница только в том, что вместо Nothing
используется Left x
, а вместо Just
– Right
.
Понятно, работает так же, как Maybe
. Это простейшая монада “ошибки с описанием”. Можно написать функции
catchError :: Either e a -> (e -> Either e' a) -> Either e' a
Left l) handler = handler l
catchError (Right r) _ = Right r
catchError (
throwError :: e -> Either e a
= Left throwError
и использовать их как проброс исключений. Например, очень многословный вариант на тему кода выше:
getCategoryForItem'' :: ItemName -> Either String CategoryName
= do
getCategoryForItem'' searchName let mItemId = fst <$> find (\(key, value) -> value == searchName) itemsTable
case mItemId of
Nothing -> throwError "Item not found"
Just itemId -> do
let mCategoryId = lookup itemId linkingTable
case mCategoryId of
Nothing -> throwError "Category for item not found"
Just catId -> do
let mCategoryName = lookup catId categoriesTable
case mCategoryName of
Nothing -> throwError "Category not found"
Just catName -> pure catName
Конечно этот же код можно гораздо проще записать в виде
getCategoryForItem'' :: ItemName -> Either String CategoryName
= do
getCategoryForItem'' searchName <- fst <$> find (\(key, value) -> value == searchName) itemsTable
itemId `onError` "Item not found"
<- lookup itemId linkingTable
categoryId `onError` "Category for item not found"
lookup categoryId categoriesTable
`onError` "Category not found"
where
0 `onError`
infix Nothing msg = throwError msg
onError Just x) msg = Right x onError (
[]
Для конструктора типа списка тоже можно определить экземпляр Monad
. Такая монада будет соответствовать нондетерминизму или многовариантности. В частности, с помощью монады списка можно очень легко написать модель недетерминированного автомата. Посмотрим на минимальное определение:
instance Functor [] where
fmap = map
instance Applicative [] where
pure x = [x]
<*> xs = concatMap (\f -> map f xs) fs
fs
instance Monad [] where
>>= f = concatMap f xs xs
Здесь можно заметить, что pure
просто создаёт список одного элемента. Операции же >>=
и <*>
используют concatMap
– если разобрать их определения, то перебираются все возможные варианты для всех элементов и возвращаются все возможные значения.
Так, например, недетерминированный конечный автомат без ε-переходов можно выразить в виде:
type State = Word
runNFA :: (State -> Char -> [State]) -> State -> [Char] -> [State]
= []
runNFA _ _ [] :restOfInput) = do
runNFA transitionTable initialState (ch<- transitionTable initialState ch
nextState runNFA transitionTable nextState restOfInput
или используя функцию foldlM
над входной строкой:
type State = Word
runNFA :: (State -> Char -> [State]) -> State -> [Char] -> [State]
= foldlM transitionTable initialState input
runNFA transitionTable initialState input -- эквивалентно
-- runNFA = foldlM
(,) a
Для пар значений оказывается тоже возможно определить экземпляр Monad
, параметризованный по типу второго значения, если первый аргумент является моноидом, т.е. поддерживает бинарную ассоциативную операцию и существует нейтральный элемент. Минимальное определение:
instance Functor ((,) a) where
fmap f (x,y) = (x, f y)
instance Monoid a => Applicative ((,) a) where
pure x = (mempty, x)
<*> (v, x) = (u <> v, f x)
(u, f)
instance Monoid a => Monad ((,) a) where
>>= k = case k a of (v, b) -> (u <> v, b) (u, a)
Writer w
Более общепринятое название для монады пары – Writer
. Эта монада представляет операции, которые могут “записывать” что-то в процессе работы. Собственно, пара – не единственная такая монада.
Вообще говоря, существует два варианта Writer
, “строгий” и “ленивый”. Вариант для пары выше – строгий, в то время как реализация Writer по умолчанию ленивая. К вопросам строгости вернёмся в следующей лекции.
Будем называть элемент, являющийся моноидом (в котором хранится то, что “записывают” вычисления) – “выводом”.
Все монады типа Writer
поддерживают операции:
writer :: (a, w) -> m a
“Заворачивает” пару значений (результат, вывод) в монаду Writer
.
tell :: w -> m ()
Добавляет аргумент к выводу
listen :: m a -> m (a, w)
“Читает” вывод аргумента и возвращает его вместе с результатом вычисления
pass :: m (a, w -> w) -> m a
Модифицирует вывод.
Для удобства вводится также функция
censor :: (w -> w) -> m a -> m a
применяющая функцию в первом аргументе к выводу второго аргумента.
Собственно, все монады, являющиеся Writer
объединяются в класс MonadWriter
, определённый в пакете mtl
в модуле Control.Monad.Writer.Class
.
Собственно, типовый представитель этого класса, Writer w a
, объявлен в модуле Control.Monad.Writer
.
Вычисления в Writer w a
производятся при помощи функции
runWriter :: Monoid w => Writer w a -> (a, w)
(->) a
Можно определить экземпляр Monad
для функций, параметризованных по возвращаемому значению. Оператор “стрелка” ->
по сути является конструктором типов двух аргументов, поэтому мы можем подойти к нему с той же позиции, что к конструкторам Either
и (,)
:
instance Functor ((->) r) where
fmap = (.)
instance Applicative ((->) a) where
pure = const
= \x -> q (f x) (g x)
liftA2 q f g
instance Monad ((->) r) where
>>= k = \r -> k (f r) r f
fmap
– это просто композиция функций. Действительно, если мы рассматриваем функцию типа a -> b
и хотим применить функцию b -> c
к её результату, то композиция – это как раз то, что нам нужно.
pure
определён как константная функция (т.е. функция, всегда возвращающая одно и то же). Действительно, если мы делаем функцию из значения, то единственный вариант, сохраняющий поведение – это функция, всегда возвращающая это значение независимо от аргумента.
liftA2
передаёт каждой из функций-аргументов одинаковый аргумент.
Оператор >>=
действует над возвращаемыми значениями функций, не изменяя аргумента.
Reader
Более общепринятое называние для монады функций – Reader
. Эта монада, по сути, отражает множество операций, которые могут “читать”, но не изменять, некое “окружение” – которое передаётся просто как аргумент функции. Вообще говоря, монада функции – не единственная, которая ведёт себя как Reader
.
Все монады, удовлетворяющие концепции Reader
поддерживают функции:
ask :: m r
Возвращает “окружение”
local :: (r -> r) -> m a -> m a
Первым аргументом принимает функцию, изменяющую окружение, и выполняет вычисление, переданное вторым аргументом, в изменённом окружении.
reader :: (r -> a) -> m a
Превращает функцию, принимающую аргумент “окружения” в вычисление в монаде.
Собственно, все монады, являющиеся Reader
объединяются в класс MonadReader
, определённый в пакете mtl
в модуле Control.Monad.Reader.Class
.
Собственно, типовый представитель этого класса, Reader r a
, объявлен в модуле Control.Monad.Reader
.
Вычисления в Reader r a
производятся при помощи функции
runReader :: Reader r a -> r -> a
State
После рассмотрения Reader
и Writer
, может возникнуть мысль, что изменяемое состояние тоже можно закодировать при помощи монады. Действительно, это возможно, и такая монада называется State
.
Тип State
традиционно определяется как
newtype State s a
= State { runState :: s -> (a, s) }
Можно провести параллели с монадой Reader
, абстрагирующей функцию, принимающую аргумент, и монадой Writer
, абстрагирующей пару.
instance Functor (State s) where
fmap f x = State $ \s ->
let (a, s') = runState x s
in (f a, s')
instance Applicative (State s) where
pure a = State $ \s -> (a, s)
State f <*> State x = State $ \ s ->
let (f, s') = f s
= x s'
(x, s'') in (f x, s'')
instance (Monad m) => Monad (State s) where
>>= k = State $ \s ->
m let (a, s') = runState m s
in runState (k a) s'
Отметим, что из-за использования let
у нас получается “ленивая” монада State
. Если бы мы использовали case
, получилась бы “строгая”. Подробное обсуждение этой темы оставим на следующую лекцию.
Собственно, это не единственная монада State
, так же как Reader
и Writer
.
Все монады, удовлетворяющие концепции State
поддерживают функции:
get :: m s
Аналогично ask
, получает текущее состояние.
put :: s -> m ()
Похоже на tell
, но не добавляет, а заменяет текущее состояние.
state :: (s -> (a, s)) -> m a
Превращает функцию, принимающую состояние и возвращающую новое состояние в вычисление в монаде.
Кроме того, есть вспомогательные функции
gets :: (s -> a) -> m a
принимает проекцию состояния на значение и возвращает применение этой проекции к текущему состоянию (состояние не изменяется)
modify :: (s -> s) -> m ()
Применяет к состоянию функцию, изменяющую его.
Собственно, все монады, являющиеся State
объединяются в класс MonadState
, определённый в пакете mtl
в модуле Control.Monad.State.Class
.
Вычисления в State s a
производятся при помощи функции
runState :: State s a -> s -> (a, s)
Кроме того есть функции
evalState :: State s a -> s -> a
execState :: State s a -> s -> s
возвращающие только результат и только финальное состояние соответственно.
С помощью State
можно моделировать любые процессы с изменяемым состоянием. Например, ДКА:
type DFAState = Word
runDFA :: (Char -> DFAState -> DFAState)
-> [Char]
-> State DFAState ()
= pure ()
runDFA transitionTable [] :restOfInput) = do
runDFA transitionTable (ch
modify (transitionTable ch) runDFA transitionTable restOfInput
или, используя функцию mapM_
(вариант mapM
, отбрасывающий результат)
runDFA :: (Char -> DFAState -> DFAState)
-> [Char]
-> State DFAState ()
= mapM_ (modify . transitionTable) input
runDFA transitionTable input -- эквивалентно
-- runDFA = mapM_ . (modify .)
ST
Мы переходим к “магическим” монадам – монадам, которые не определяются в терминах стандартного Haskell, и задача которых – обеспечить взаимодействие с “внешним миром”. Монада ST
абстрагирует работу с памятью и позволяет работать с изменяемыми участками памяти, в частности, с переменными (в императивном смысле) и массивами (в смысле непрерывных участков изменяемой памяти).
Обычно, монада ST
не нужна. Однако, некоторые алгоритмы значительно проще (и эффективнее!) в императивной парадигме. Поэтому иногда оказывается очень удобно вставить кусочек “почти императивного” кода в функциональный.
На самом деле с теоретической точки зрения, монада ST
ничем не отличается от монады State
. Однако реализация у них – сильно разная.
Основные функции для работы с изменяемыми значениями в монаде ST
:
newSTRef :: a -> ST s (STRef s a)
Создаёт новую переменную и инициализирует её значением первого аргумента.
readSTRef :: STRef s a -> ST s a
Читает значение переменной.
writeSTRef :: STRef s a -> a -> ST s ()
Записывает значение в переменную.
IO
Монада IO
абстрагирует всю работу с “внешним миром”, которая в целом сводится к вводу-выводу. О ней можно думать как о более сложном варианте монады ST
, где в качестве состояния используется тип RealWorld
, абстрагирующий состояние “внешнего мира”.
Ключевое отличие IO
от ST
и State
в том, что RealWorld
– на самом деле своего рода фикция: на самом деле никакого “состояния” RealWorld
в скомпилированной программе нет (в отличие от State
где для хранения состояния используется участок памяти). Вместо этого, использование IO
сигнализирует компилятору, что порядок операций в IO
при выполнении должен быть таким же, как в исходном тексте программы, и что нельзя применять многие оптимизации, которые можно применять в функционально чистом коде – в частности, нельзя “выбрасывать” повторяющиеся вычисления, поскольку состояние “внешнего мира” могло измениться и, следовательно, результат вычислений тоже мог измениться.
Любое значение в программе на Haskell, имеющее тип IO a
является своего рода инструкцией, как получить значение a
, взаимодействуя с внешним миром. Эта инструкция является “чистой”, в смысле сама по себе не имеет побочных эффектов. Её можно передавать как значение, над ней можно совершать манипуляции, как над любым другим значением в монаде. Эта “инструкция” будет выполнена тогда и только тогда, когда она будет объединена с IO
в функции main
(главной точке входа в программу). И именно тогда возникнут все связанные побочные эффекты. Под “объединением” имеется ввиду следующая операция. В модуле Control.Monad
объявлена функция join
:
join :: (Monad m) => m (m a) -> m a
= x >>= id join x
Как видно из типа этой функции, она убирает один уровень вложенности какой-то монады m
(например, она работает как конкатенация для монады списка). Определение этой функции просто применяет оператор >>=
к функции идентичности.
То есть, если какое-то действие IO a
в какой-то момент передаётся в вычисление, которое в результате вернётся как результат функции main
, это действие IO a
будет выполнено.
В качестве примера, рассмотрим два фрагмента кода:
main :: IO ()
= do
main let action = putStrLn "Hello, World!"
return ()
Этот код ничего не делает – вызов putStrLn
никогда не объединяется с IO
в main
. Действительно, если записать этот код без do-нотации, то получится
main :: IO ()
= let action = putStrLn "Hello, World!" in return () main
main :: IO ()
= do
main <- putStrLn "Hello, World!"
action return ()
Этот код выведет строку Hello, World!
, поскольку, если убрать do
-нотацию, мы увидим:
main :: IO ()
= putStrLn "Hello, World!" >>= \action -> return () main
Несмотря на то, что значение action
(которое в силу типа putStrLn
является единичным ()
) не используется, действие, необходимое для получения этого значения (а именно, putStrLn "Hello, World!"
) должно быть выполнено перед тем, как функция main
сможет вернуть результат ()
, поскольку оно объединяется с результатом main
оператором >>=
.
Первый код можно исправить следующим образом:
main :: IO ()
= do
main let action = putStrLn "Hello, World!"
actionreturn ()
Без do-нотации:
= let action = putStrLn "Hello, World!" in action >>= \_ -> return () main
или, эквивалентно
= let action = putStrLn "Hello, World!" in action >> return () main
Композиция типов
Часто бывает нужна монада, которая например ведёт себя одновременно, например, как Reader и Writer. Для этого используются трансформаторы монад. Рассмотрим сначала более простую идею: композицию типов в общем.
Рассмотрим функции
id :: a -> a
id x = x
(.) :: (b -> c) -> (a -> b) -> a -> c
. g) = \x -> f (g x) (f
Проводя аналогию между функциями над значениями и конструкторами типов (над типами), мы можем записать следующие аналогичные определения типов. Начнём с id
.
newtype Identity a = Identity { runIdentity :: a}
Типы в Haskell бывают различными: мы уже сталкивались с этим, когда говорили про некоторые классы типов, определённые для конструкторов типов. Haskell использует понятие “род” (kind) для классификации типов.
Обычные типы имеют род Type
или *
(это синонимы). Конструкторы типов одного аргумента имеют род Type -> Type
или * -> *
, конструкторы типов двух аргументов имеют род Type -> Type -> Type
или * -> * -> *
и так далее.
В GHCi можно вывести род типа, выполнив команду :kind Typename
. Так, объявленный выше тип Identity
будет иметь род * -> *
, поскольку это конструктор типа одного аргумента.
Identity
является монадой (т.е. можно написать экземпляры Functor
, Applicative
и Monad
):
instance Functor Identity where
fmap f (Identity x) = Identity (f x)
instance Applicative Identity where
pure = Identity
Identity f) <*> (Identity x) = Identity (f x)
(
instance Monad Identity where
Identity x) >>= f = f x (
Чтобы написать аналог оператора (.)
на уровне типов, нам нужно сконструировать тип, имеющий род (* -> *) -> (* -> *) -> * -> *
(ср. с типом оператора (.)
).
Глядя на сигнатуру рода, мы можем сделать вывод, что это конструктор типа трёх аргументов, два из которых – конструкторы одного аргумента, а третий – обычный тип:
newtype Compose f g a =
Compose { getCompose :: f (g a) }
(это определение есть в модуле Data.Functor.Compose
)
На практике, это может выглядеть, например так:
> 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]
.
Мы сравнительно легко можем написать экземпляры 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
нас ожидает трудность:
instance (Monad f, Monad g)
=> Monad (Compose f g) where
Compose x) >>= f
(= ???
Не будем останавливаться на доказательстве того, что это невозможно1, лишь отметим, что тот же подход, что и с функторами не работает.
Трансформаторы монад
Проблема с монадами в том, что можно записать композицию двух монад, но композиция двух монад сама по себе не является монадой.
В связи с этой трудностью вводится понятие трансформатора монады. Трансформатор монады – это конструктор типа, который принимает монаду в качестве аргумента и возвращает новую монаду.
Основная проблема композиции двух монад – в недостатке информации. Если фиксировать “внешнюю” монаду, то мы, собственно, и получим трансформатор монад. Таким образом, для каждой монады существует так же и трансформатор монад.
Рассмотрим введённую выше монаду Identity:
newtype Identity a = Identity { runIdentity :: a }
Как может выглядеть трансформатор монады IdentityT
?
newtype IdentityT m a =
IdentityT { runIdentityT :: m (Identity a) }
Мы добавили новый аргумент типа, имеющий род * -> *
к конструктору Identity
, и можем получить нижележащее значение IdentityT
, выполнив runIdentityT
. Эта конструкция в каком-то смысле эквивалентна условной конструкции
data Compose m Identity a
= Compose { getCompose :: m (Identity a) }
(это строго говоря некорректный синтаксис)
Давайте попробуем описать монаду для 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
такой возможности мы не имели, поскольку интерфейс монады в общем случае не поддерживает универсального способа её “выполнить” (в случае например IO
это невозможно в терминах стандартного Haskell)
Рассмотрим теперь некоторые стандартные трансформаторы монад.
MaybeT
newtype MaybeT m a =
MaybeT { runMaybeT :: m (Maybe a) }
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
instance Monad m => Monad (MaybeT m) where
MaybeT x) >>= f
(= MaybeT $
>>= maybe (pure Nothing)
x . f) (runMaybeT
Экземпляры Functor
и Applicative
не представляют из себя ничего интересного, это экземпляры для композиции типов.
Экземпляр Monad
здесь работает аналогично экземпляру для Maybe
, что может быть проще увидеть, если переписать код через do-нотацию:
instance Monad m => Monad (MaybeT m) where
MaybeT x) >>= f = MaybeT $ do
(<- x
ma case ma of
Nothing -> pure Nothing
Just x -> runMaybeT (f x)
Для сравнения:
instance Monad Maybe where
>>= k = case x of
x Nothing -> Nothing
Just y -> k y
EitherT
newtype EitherT e m a =
EitherT { runEitherT :: m (Either e a) }
instance Monad m => Monad (EitherT e m) where
EitherT x) >>= f
(= EitherT $
>>= either (pure . Left)
x . f) (runEitherT
Здесь опустим экземпляры для Functor
и Applicative
, они практически одинаковые для всех трансформаторов монад.
Если переписать экземпляр Monad
через do-нотацию:
instance Monad m => Monad (EitherT e m) where
EitherT x) >>= f = EitherT $ do
(<- x
ea case ea of
Left e -> pure (Left e)
Right x -> runEitherT (f x)
Для сравнения:
instance Monad (Either e) where
>>= f = case x of
x Left e -> Left e
Right x -> 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
<- x r
y runReaderT (f y) r
Для сравнения:
instance Monad (Reader r) where
Reader x) >>= f
(= Reader $ \r -> runReader (f (x r)) r
StateT
newtype StateT s m a =
StateT { runStateT :: s -> m (a, s) }
instance Monad m => Monad (StateT s m) where
>>= f
x = StateT $ \s -> do
~(a, s') <- runStateT x s
runStateT (f a) s'
Для сравнения:
instance (Monad m) => Monad (State s) where
>>= k = State $ \s ->
x let (a, s') = runState x s
in runState (k a) s'
Обычные монады из трансформаторов монад
На самом деле, для монад Reader
, State
и т.п. “простые” монады не определяются явно, вместо этого они определяются в терминах трансформаторов. Например, Reader
можно легко получить из ReaderT
, просто применив его к Identity
:
type Reader r a = ReaderT r Identity a
runReader :: Reader r a -> r -> a
= runIdentity (runReaderT m r) runReader m r
Порядок записи и порядок вложенности
Порядок записи типов трансформаторов монад может быть несколько неинтуитивным в смысле того, что представляют значения и в каком порядке их вычислять.
Рассмотрим для примера следующий код:
mval :: EitherT String (
MaybeT (
ReaderT () Identity
)Int
) = return 0 mval
Чтобы “вычислить” весь этот стек монад, мы должны выполнить все трансформаторы, начиная с внешнего:
= runIdentity (
val
runReaderT (
runMaybeT (runEitherT mval) ) ())
Отметим, что в силу порядка применения функций, синтаксически вычисление “внешнего” трансформатора в типе оказывается “внутренним” выражением.
но какой тип будет у val
? Вспомним, что
runEitherT :: EitherT e m a -> m (Either e a)
Тогда тип x = runEitherT mval
будет
x :: MaybeT (ReaderT () Identity)
Either String Int) (
Аналогично,
runMaybeT :: MaybeT m a -> m (Maybe a)
и тип y = runMaybeT x
будет
y :: ReaderT () Identity
Maybe (Either String Int)) (
Наконец,
runReaderT :: r -> m a
и тип z = runReader y ()
z :: Identity (Maybe (Either String Int))
после чего runIdentity
просто убирает “лишний” слой Identity
.
Что здесь примечательно: трансформаторы вычисляются от “внешнего” к “внутреннему” в записи типа, но значение, которое получается, имеет монаду внешнего трансформатора на внутренней позиции и т.д. Это прямо связано с порядком вычисления и с необходимостью иметь монаду на внешнем уровне вложенности после “выполнения” каждого трансформатора (если бы это было не так, мы не могли бы последовательно применять функции runSomethingT
).
Другие трансформаторы монад
Можно заметить, что мы не рассматривали трансформаторы для монад списков и Writer
. Это связано в основном с тем, что их не так просто написать.
Практически, WriterT
, как и Writer
используется достаточно редко, в основном поскольку он подходит только для достаточно специфических применений. В частности, он не слишком хорошо подходит для логгирования.
Что касается трансформатора списка, наивная реализация, доступная в библиотеке transformers
, не удовлетворяет закону ассоциативности, как следствие, приводит к неожиданным результатам. Корректная реализация же (доступная в библиотеке list-t) достаточно сложная и медленная. На практике вместо трансформаторов списка используются библиотеки, работающие с “потоками”, такие как streaming
, conduit
, pipes
, или streamly
. Для восприятия, возможно, проще всего streamly
, conduit
пока, видимо, наиболее мощный, но довольно сложный инструмент.
MonadTrans
Для трансформаторов монад обычно пишется экземпляр класса MonadTrans
:
class MonadTrans t where
lift :: (Monad m) => m a -> t m a
Функция 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
= do
doStuff a b <- lift $ safeIntegerDivision a b
val == 0) $ throwError "Value is zero!"
when (val return val
Здесь функция safeIntegerDivision
возвращает Maybe Int
, но с помощью lift
мы используем этот результат в контексте ExceptT String Maybe Int
. Одно применение lift
“поднимает” один уровень стека. Идеологически, принцип похож на pure
с тем отличием, что вместо “чистых” значений, “поднимаются” значения в монадах.
Реализация MonadTrans
в большинстве случаев достаточно простая, например
instance MonadTrans MaybeT where
lift :: (Monad m) => m a -> MaybeT m a
= MaybeT $ fmap Just ma lift ma
Некоторая проблема с использованием lift
заключается в том, что наивное использование приводит к коду типа такого
$ lift $ lift $ lift $ lift $ lift $ lift $ put i' 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
= ReaderExceptMaybeT . lift . lift . lift lift
И в случае необходимости, написание вспомогательных функций:
liftREMTMaybe :: Monad m
=> MaybeT m a
-> ReaderExceptMaybeT r e m a
= ReaderExceptMaybeT . lift . lift
liftREMTMaybe
liftREMTExcept :: Monad m
=> ExceptT e (MaybeT m) a
-> ReaderExceptMaybeT r e m a
= ReaderExceptMaybeT . lift liftREMTExcept
MonadIO
Заходящий с другого конца, есть класс, позволяющий “поднимать” действия в IO
сразу до верха стека трансформаторов:
class (Monad m) => MonadIO m where
liftIO :: IO a -> m a
Экземпляр при наличии экземпляра MonadTrans
можно объявить следующим образом:
instance MonadIO m
=> MonadIO (ReaderExceptMaybeT r e m) where
= lift . liftIO liftIO
Для интересующихся, http://web.cecs.pdx.edu/~mpj/pubs/RR-1004.pdf, стр. 44↩︎