И мы знаем, как к этому числу можно применить функцию:
Достаточно просто. А теперь усложним задание — пусть наше значение имеет контекст. Пока что вы можете думать о контексте просто как о ящике, куда можно положить значение:
Теперь, применяя функцию к значению, вы будете получать разные значения в зависимости от контекста. Это основная идея, на которой базируются функторы, аппликативные функторы, монады, стрелки и т.п. Тип данных Maybe
определяет два связанных контекста:
data Maybe a = Nothing | Just a
Позже мы увидим, как изменяется применение функции, когда что-то является Just a
vs Nothing
. Но сначала давайте поговорим о функторах!
Функторы
Когда у вас есть значение, обёрнутое в контекст, вы не можете просто взять и применить к нему обычную функцию:
И здесь fmap
спешит на помощь. fmap
— парень с улицы, fmap
знает толк в контекстах. Он в курсе, как применить функцию к значению, обёрнутому в контекст. Например, предположим, что вы хотите применить (+3)
к Just 2
. Используйте fmap
:
> fmap (+3) (Just 2) Just 5
Бам! fmap
показал нам, как это делается! Но откуда он знает, как правильно применять функцию?
Так что такое функтор на самом деле?
Функтор — это тип классов. Вот его определение:
Функтором является любой тип данных, для которого определено, как к нему применяется fmap
. А вот как fmap
работает:
Так что мы можем делать так:
> fmap (+3) (Just 2) Just 5
И fmap
магическим образом применит эту функцию, потому что Maybe
является функтором. В нём определено, как применять функции к Just
‘ам и Nothing
‘ам:
instance Functor Maybe where fmap func (Just val) = Just (func val) fmap func Nothing = Nothing
Вот что происходит за сценой, когда мы пишем fmap (+3) (Just 2)
:
А потом вы скажете: «Ладно, fmap
, а примени-ка, пожалуйста, (+3)
к Nothing
.»
> fmap (+3) Nothing Nothing
Билл О’Рейли ничегошеньки не смыслит в Maybe функторе
Как Морфеус в «Матрице», fmap
знает, что делать; вы начали с Nothing
и закончите тоже с Nothing
! Это fmap
-дзен. И теперь понятно, для чего вообще существует тип данных Maybe
. Вот, например, как бы вы работали с записью в базе данных на языке без Maybe
:
post = Post.find_by_id(1) if post return post.title else return nil end
На Haskell же:
fmap (getPostTitle) (findPost 1)
Если findPost
возвращает пост, то мы выдаём его заголовок с помощью getPostTitle
. Если же он возвращает Nothing
, то и мы возвращаем Nothing
! Чертовски изящно, а?
<$>
— инфиксная версия fmap
, так что вместо кода выше вы частенько можете встретить:
getPostTitle <$> (findPost 1)
А вот ещё один пример: что происходит, когда вы применяете функцию к списку?
Списки тоже функторы! Вот определение:
instance Functor [] where fmap = map
Ладно, ладно, ещё один (последний) пример: что случится, когда вы примените функцию к другой функции?
fmap (+3) (+1)
Вот эта функция:
А вот функция, применённая к другой функции:
Результат — просто ещё одна функция!
> import Control.Applicative > let foo = fmap (+3) (+2) > foo 10 15
Так что функции — тоже функторы!
instance Functor ((->) r) where fmap f g = f . g
И когда вы применяете fmap
к функции, то попросту делаете композицию функций!
Аппликативные функторы
Следующий уровень — аппликативные функторы. С ними наше значение по-прежнему обёрнуто в контекст (так же как с функторами):
Но теперь в контекст обёрнута и наша функция!
Ага! Давайте-ка нырнём поглубже. Аппликативные функторы надувательством не занимаются. Control.Applicative
определяет <*>
, который знает, как применить функцию, обёрнутую в контекст, к значению, обёрнутому в контекст:
Т.е.
Just (+3) <*> Just 2 == Just 5
Использование <*>
может привести к некоторым интересным ситуациям. Например:
> [(*2), (+3)] <*> [1, 2, 3] [2, 4, 6, 4, 5, 6]
А вот кое-что, что вы можете сделать с аппликативными функторами, но не сможете с обычными. Как вы примените функцию, которая принимает два аргумента, к двум обёрнутым значениям?
> (+) <$> (Just 5) Just (+5) > Just (+5) <$> (Just 4) ОШИБКА??? ЧТО ЭТО ВООБЩЕ ЗНАЧИТ ПОЧЕМУ ФУНКЦИЯ ОБЁРНУТА В JUST
Аппликативные функторы:
> (+) <$> (Just 5) Just (+5) > Just (+5) <*> (Just 3) Just 8
Applicative
технично отодвигает Functor
в сторону. «Большие парни могут использовать функции с любым количеством аргументов,» — как бы говорит он. — «Вооружённый <$>
и <*>
, я могу взять любую функцию, которая ожидает любое число необёрнутых аргументов. Затем я передам ей все обёрнутые значения и получу обёрнутый же результат! БВАХАХАХАХАХА!»
> (*) <$> Just 5 <*> Just 3 Just 15
Аппликативный функтор наблюдает за тем, как обычный применяет функцию
И да! Существует функция liftA2
, которая делает тоже самое:
> liftA2 (*) (Just 5) (Just 3) Just 15
Монады
Как изучать монады:
- Получить корочки PhD в Computer Science
- Выкинуть их нафиг, потому что при чтении этого раздела они вам не понадобятся!
Монады добавляют новый поворот в нашем сюжете.
Функторы применяют обычную функцию к обёрнутому значению.
Аппликативные функции применяют обёрнутую функцию к обёрнутому значению
Монады применяют функцию, которая возвращает обёрнутое значение, к обёрнутому значению. У монад есть функция >>=
(произносится «bind»), позволяющая делать это.
Рассмотрим такой пример: наш старый добрый Maybe
— это монада:
Просто болтающаяся монада
Пусть half
— функция, которая работает только с чётными числами:
half x = if even x then Just (x `div` 2) else Nothing
А что, если мы скормим ей обёрнутое значение?
Нам нужно использовать >>=
, чтобы пропихнуть обёрнутое значение через функцию. Вот фото >>=
:
А вот как она работает:
> Just 3 >>= half Nothing > Just 4 >>= half Just 2 > Nothing >>= half Nothing
Что же происходит внутри? Monad
— ещё один класс типов. Вот его частичное определение:
class Monad m where (>>=) :: m a -> (a -> m b) -> m b
Где >>=
:
Так что Maybe
— это монада:
instance Monad Maybe where Nothing >>= func = Nothing Just val >>= func = func val
Вот какие действия происходят над бедным Just 3
!
А если вы подадите на вход Nothing
, то всё ещё проще:
Можно так же связать цепочку из вызовов:
> Just 20 >>= half >>= half >>= half Nothing
Клёвая штука! И теперь мы знаем, что Maybe
— это Functor
, Applicative
и Monad
в одном лице.
А сейчас давайте переключимся на другой пример: IO
монаду:
В частности, на три её функции. getLine
не принимает аргументов и получает пользовательские данные с входа:
getLine :: IO String
readFile
принимает строку (имя файла) и возвращает его содержимое:
readFile :: FilePath -> IO String
putStrLn
принимает строку и печатает её:
putStrLn :: String -> IO ()
Все три функции принимают регулярные значения (или вообще не принимают значений) и возвращают обёрнутые значения. Значит, мы можем связать их с помощью >>=
!
getLine >>= readFile >>= putStrLn
О да, у нас билеты в первый ряд на «Монады-шоу»!
Haskell так же предоставляет нам немного синтаксического сахара для монад, называемого do-нотация:
foo = do filename <- getLine contents <- readFile filename putStrLn contents
Заключение
ссылка на оригинал статьи http://habrahabr.ru/post/182466/
Добавить комментарий