liftM и компания
Когда мы начали своё путешествие на верхушку Горы Монад, мы сначала посмотрели на функторы, предназначенные для сущностей, которые можно отображать. Затем рассмотрели улучшенные функторы – аппликативные, которые позволяют нам применять обычные функции между несколькими аппликативными значениями, а также брать обычное значение и помещать его в некоторый контекст по умолчанию. Наконец, мы ввели монады как улучшенные аппликативные функторы, которые добавляют возможность тем или иным образом передавать эти значения с контекстом в обычные функции.
Итак, каждая монада – это аппликативный функтор, а каждый аппликативный функтор – это функтор. Класс типов Applicative имеет такое ограничение класса, ввиду которого наш тип должен иметь экземпляр класса Functor, прежде чем мы сможем сделать для него экземпляр класса Applicative. Класс Monad должен иметь то же самое ограничение для класса Applicative, поскольку каждая монада является аппликативным функтором – однако не имеет, потому что класс типов Monad был введён в язык Haskell задолго до класса Applicative.
Но хотя каждая монада – функтор, нам не нужно полагаться на то, что у неё есть экземпляр для класса Functor, в силу наличия функции liftM. Функция liftM берёт функцию и монадическое значение и отображает монадическое значение с помощью функции. Это почти одно и то же, что и функция fmap! Вот тип функции liftM:
liftM :: (Monad m) => (a –> b) –> m a –> m b
Сравните с типом функции fmap:
fmap :: (Functor f) => (a –> b) –> f a –> f b
Если экземпляры классов Functor и Monad для типа подчиняются законам функторов и монад, между этими двумя нет никакой разницы (и все монады, которые мы до сих пор встречали, подчиняются обоим). Это примерно как функции pure и return, делающие одно и то же, – только одна имеет ограничение класса Applicative, тогда как другая имеет ограничение Monad.
Давайте опробуем функцию liftM:
ghci> liftM (*3) (Just 8)
Just 24
ghci> fmap (*3) (Just 8)
Just 24
ghci> runWriter $ liftM not $ Writer (True, "горох")
(False,"горох")
ghci> runWriter $ fmap not $ Writer (True, "горох")
(False,"горох")
ghci> runState (liftM (+100) pop) [1,2,3,4]
(101,[2,3,4])
ghci> runState (fmap (+100) pop) [1,2,3,4]
(101,[2,3,4])
Вы уже довольно хорошо знаете, как функция fmap работает со значениями типа Maybe. И функция liftM делает то же самое. При использовании со значениями типа Writer функция отображает первый компонент кортежа, который является результатом. Выполнение функций fmap или liftM с вычислением, имеющим состояние, даёт в результате другое вычисление с состоянием, но его окончательный результат изменяется добавленной функцией. Если бы мы не отобразили функцию pop с помощью (+100) перед тем, как выполнить её, она бы вернула (1, [2,3,4]).
Вот как реализована функция liftM:
liftM :: (Monad m) => (a –> b) –> m a –> m b
liftM f m = m >>= (x –> return (f x))
Или с использованием нотации do:
liftM :: (Monad m) => (a –> b) –> m a –> m b
liftM f m = do
x <– m
return (f x)
Мы передаём монадическое значение m в функцию, а затем применяем функцию к его результату, прежде чем поместить его обратно в контекст по умолчанию. Ввиду монадических законов гарантируется, что функция не изменит контекст; она изменяет лишь результат, который представляет монадическое значение.
Вы видите, что функция liftM реализована совсем не ссылаясь на класс типов Functor. Значит, мы можем реализовать функцию fmap (или liftM – называйте, как пожелаете), используя лишь те блага, которые предоставляют нам монады. Благодаря этому можно заключить, что монады, по крайней мере, настолько же сильны, насколько и функторы.
Класс типов Applicative позволяет нам применять функции между значениями с контекстами, как если бы они были обычными значениями, вот так:
ghci> (+) <$> Just 3 <*> Just 5
Just 8
ghci> (+) <$> Just 3 <*> Nothing
Nothing
Использование этого аппликативного стиля всё упрощает. Операция <$> – это просто функция fmap, а операция <*> – это функция из класса типов Applicative, которая имеет следующий тип:
(<*>) :: (Applicative f) => f (a –> b) –> f a –> f b
Так что это вроде fmap, только сама функция находится в контексте. Нам нужно каким-то образом извлечь её из контекста и с её помощью отобразить значение f a, а затем вновь собрать контекст. Поскольку все функции в языке Haskell по умолчанию каррированы, мы можем использовать сочетание из операций <$> и <*> между аппликативными значениями, чтобы применять функции, принимающие несколько параметров.
Однако, оказывается, как и функция fmap, операция <*> тоже может быть реализована, используя лишь то, что даёт нам класс типов Monad. Функция ap, по существу, – это <*>, только с ограничением Monad, а не Applicative. Вот её определение:
ap :: (Monad m) => m (a –> b) –> m a –> m b
ap mf m = do
f <– mf
x <– m
return (fx)
Функция ap – монадическое значение, результат которого – функция. Поскольку функция, как и значение, находится в контексте, мы берём функцию из контекста и называем её f, затем берём значение и называем его x, и, в конце концов, применяем функцию к значению и представляем это в качестве результата. Вот быстрая демонстрация:
ghci> Just (+3) <*> Just 4
Just 7
ghci> Just (+3) `ap` Just 4
Just 7
ghci> [(+1),(+2),(+3)] <*> [10,11]
[11,12,12,13,13,14]
ghci> [(+1),(+2),(+3)] `ap` [10,11]
[11,12,12,13,13,14]
Теперь нам видно, что монады настолько же сильны, насколько и аппликативные функторы, потому что мы можем использовать методы класса Monad для реализации функций из класса Applicative. На самом деле, когда обнаруживается, что определённый тип является монадой, зачастую сначала записывают экземпляр класса Monad, а затем создают экземпляр класса Applicative, просто говоря, что функция pure – это return, а операция <*> – это ap. Аналогичным образом, если у вас уже есть экземпляр класса Monad для чего-либо, вы можете сделать для него экземпляр класса Functor, просто говоря, что функция fmap – это liftM.
Функция liftA2 весьма удобна для применения функции между двумя аппликативными значениями. Она определена вот так:
liftA2 :: (Applicative f) => (a –> b –> c) –> f a –> f b –> f c
liftA2 f x y = f <$> x <*> y
Функция liftM2 делает то же, но с использованием ограничения Monad. Есть также функции liftM3, liftM4 и liftM5.
Вы увидели, что монады не менее сильны, чем функторы и аппликативные функторы – и, хотя все монады, по сути, являются функторами и аппликативными функторами, у них необязательно имеются экземпляры классов Functor и Applicative. Мы изучили монадические эквиваленты функций, которые используются функторами и аппликативными функторами.