13 июня 2013 в 10:41

Функторы, аппликативные функторы и монады в картинках перевод

Вот некое простое значение:


И мы знаем, как к нему можно применить функцию:


Элементарно. Так что теперь усложним задание — пусть наше значение имеет контекст. Пока что вы можете думать о контексте просто как о ящике, куда можно положить значение:


Теперь, когда вы примените функцию к этому значению, результаты вы будете получать разные — в зависимости от контекста. Это основная идея, на которой базируются функторы, аппликативные функторы, монады, стрелки и т.п. Тип данных Maybe определяет два связанных контекста:


data Maybe a = Nothing | Just a

Позже мы увидим разницу в поведении функции для Just a против 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


Монады


Как изучать монады:
  1. Получить корочки PhD в Computer Science
  2. Выкинуть их нафиг, потому что при чтении этого раздела они вам не понадобятся!

Монады добавляют новый поворот в наш сюжет.
Функторы применяют обычную функцию к упакованному значению:


Аппликативные функторы применяют упакованную функцию к упакованному же значению:


Монады применяют функцию, которая возвращает упакованное значение, к упакованному значению. У монад есть функция >>= (произносится «связывание» (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


Заключение


  1. Функтор — это тип данных, реализуемый с помощью класса типов Functor
  2. Аппликативный функтор — это тип данных, реализуемый с помощью класса типов Applicative
  3. Монада — это тип данных, реализуемый с помощью класса типов Monad
  4. Maybe реализуется с помощью всех трёх классов типов, поэтому является функтором, аппликативным функтором и монадой одновременно

В чём разница между этими тремя?


  • функтор: вы применяете функцию к упакованному значению, используя fmap или <$>
  • аппликативный функтор: вы применяете упакованную функцию к упакованному значению, используя <*> или liftA
  • монада: вы применяете функцию, возвращающую упакованное значение, к упакованному значению, используя >>= или liftM


Итак, дорогие друзья (а я надеюсь, что к этому моменту мы стали друзьями), я думаю, все мы согласимся с тем, что монады простая и УМНАЯ ИДЕЯ (тм). А теперь, после того, как мы промочили горло этим руководством, то почему бы не позвать Мела Гибсона и не допить бутылку до дна? Проверьте раздел, посвящённый монадам, в LYAH. Там очень много вещей, о которых я умолчал, потому что Миран проделал великолепную работу по углублению в этот материал.
Ещё больше монад и картинок можно найти в трёх полезных монадах (перевод).

От переводчика:
Ссылка на оригинал: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html Пишу её так, потому что Хабр ругается на url с запятыми.
И, конечно, я буду очень признательна за замечания в личку относительно перевода.
+166
62170
801
AveNat 89,5

Комментарии (58)

+11
roman_kashitsyn, #
Большое спасибо за публикацию такой красочной статьи :)
Напомнило Learn You a Haskell, только картинок больше. Реквестую аналогичные статьи по трансформерам монад, комонадам и линзам. Кстати, вот этот ресурс показался мне весьма полезным и доступным Haskell For All, кажется, статья по линзам там самая доступная.
0
maksbotan, #
Ох чёрт! Что такое линзы? От обилия новых странных терминов ощущение, что на лекции по алгебре.
+1
ArtyomKazak, #
Линзы — это чудные штуковины. Смотрите: чтобы достать первый элемент пары, есть fst. Чтобы получить i-тый элемент списка, есть (!!). А чтобы изменить элемент пары или значение в списке, ничего нету. Пока не придумали линзы, разбирать структуры данных было гораздо легче, чем собирать их назад. А теперь getter и setter объединены в одной функции, и эти функции-линзы даже образуют категорию (т.е. (.) работает для них так же, как и для обычных функций).
+4
catlion, #
Это очень просто: Lenses are coalgebras of the costate comonad
+4
vittore, #
Титанический труд с переводом всех этих волшебных картинок! Статья отличная, давно уже в закладках, теперь можно тем, кто по-английски не читает ссылку давать!
+1
HaruAtari, #
Прекрасная статья. Спасибо большое за проделанную работу.
Отдельное спасибо за перевод иллюстраций.
+1
tvolf, #
Тоже присоединяюсь к товарищам выше и выражаю свою благодарность за труд. Спасибо.
+1
Ogoun, #
Я далек от haskell, поясните, пожалуйста, если аппликативный функтор работает с упакованными функциями и значениями, в чем отличие его от монады? Ведь судя по рисунку он возвращает так же как и монада упакованное значение.
Что нам мешает писать
getLine <*> readFile <*> putStrLn
вместо
getLine >>= readFile >>= putStrLn?

И правильно ли я понимаю что контекст значения, это аналог класса, а контекст функции это ее ограничения по работе с данными?
+1
roman_kashitsyn, #
Аппликативные функторы накладывают меньше ограничений, т.е. являются более общей (соответственно, менее «мощной») сущностью. Каждая монада является аппликативным функтором, но не каждый аппликативный функтор является монадой. Так что всё, что справедливо для АФ, справедливо для монад.

Ну а аналог getLine <*> readFile <*> putStrLn написать не получится, т.к. типы не сойдутся: <*> имеет тип f (a -> b) -> f a -> f b, т.е. применяет функцию внутри коробки — функтора. readFile же берёт чистое значение и возвращает упакованное (String -> IO String), и в итоге получается, что строка с текстом файла будет вложена в коробку дважды (IO (IO String)):

Prelude Control.Applicative> :t (pure readFile <*> getLine)
(pure readFile <*> getLine) :: IO (IO String)


Именно возможностью уменьшать глубину вложенности коробок, т.е. наличием возможности реализации join :: m (m a) -> m a монада и отличается от АФ. Грубо говоря, монада имеет право распаковывать значения из коробок, а АФ — только выполнять функции, не распаковывая коробок.
+1
mikhanoid, #
Как всегда в таких случаях, мне очень хочется спросить: а зачем это всё нужно? То есть, в контексте Haskell это всё понятно, ввели такую систему типов. Но зачем это всё в реальном мире? Есть примеры? Почему в Real World Haskell почти во всех примерах решение идёт через foreign, небезопасные массивы и прочие прелести? Где же там мощь функторов и монад? Имеют ли они вообще какой-нибудь не абстрактный смысл вне Haskell?
+5
jakobz, #
Обычный язык программирования — типа javascript-а, с точки зрения хаскеля всегда выполняется сразу в нескольких, определенных разработчиками языка, монадах. Как правило — монад для ввода-вывода, исключений и хранения состояния. В хаскеле эти понятия разделены, каждое используются где оно нужно, и допускается создание своих сущностей такого типа.

Это нужно для большей гибкости, и того же «separation of concerns». Если функция читает из базы и логирует — это написано в ее типе, и ничего другого она делать не может, например не сможет иметь глобальных переменных.

Можно посмотреть и с другой стороны — монады позволяют строить себе внутри языка свои маленькие языки очень малой кровью — за счет того, что монады стандартны, есть спец. синтаксис, и есть много функций в библиотеке (тот же _forM — чтобы циклы каждый раз самому не делать).
+1
mikhanoid, #
Так, в общем-то, любой язык с поддержкой библиотек позволяет такие языки строить. Вот чем, например, OpenGL не является DSL-ем? Или там, например, POSIX?

В том же javascript (да или хотя бы в Pascal) явно не нужно прописывать монады или те же аппликативные функторы, они возникают сами и их можно вывести из структуры графа программы. И при этом, структуру этих монад и прочих механизмов управления выполнением можно менять очень легко, фактически, просто меняя ссылки на переменные в выражениях. В Haskell же не так всё просто, потому что всё слишком формально и не-автоматически: нельзя жить сразу в нескольких монадах, например, нужно всегда вкладывать одну в другую, а потом постоянно разворачивать и сворачивать этот стек. Не особо удобно.

Поэтому у меня создаётся ощущение: нам предлагают некий механизм, которым сложно управлять, который сложно читать и понимать, и который не особо добавляет выразительности программам (если смотреть не только на Haskell, но и на другие языки). Зачем? В чём великий плюс?
–1
chemistmail, #
Как всегда в таких случаях, мне очень хочется спросить: а зачем это всё нужно?

Чтобы писать программы.

То есть, в контексте Haskell это всё понятно, ввели такую систему типов. Но зачем это всё в реальном мире?

Затем же зачем нужны C, C++, Java, Python и остальные языки программирования.

Есть примеры?

Есть.
http://hackage.haskell.org/packages/archive/pkg-list.html

Почему в Real World Haskell почти во всех примерах решение идёт через foreign, небезопасные массивы и прочие прелести?

foreign — в книге всего одна глава из 28, и это далеко не все примеры.
небезопасные массивы тоже есть, но в очень малом количестве случаев.

Где же там мощь функторов и монад?

Может вначале почитать книгу?

Имеют ли они вообще какой-нибудь не абстрактный смысл вне Haskell?

Любой оператор, любого языка программирования суть есть абстракция.
+3
mikhanoid, #
Чтобы писать программы.

Писать программы можно и пробиванием дырок гвоздиком на перфолентах. Но зачем-то человечество придумало различные механизмы в программировании. Например, я понимаю, зачем придуманы механизм переменных в императивных языках, или зачем придуман механизм агентов в языках функциональных, или зачем придуман механизм сообщений в Erlang. Я понимаю, чем они хороши для практики программирования. Но я за долгое время так и не смог понять, чем хороши монады. Я понимаю, зачем они нужны в чистом функциональном языке, но я не понимаю, почему это это всё полезно и удобно при условии существования кучи других языков программирования. Я не понимаю, в чём «изящество» и мощь этих механизмов.

И у меня есть стойкое ощущение, что единственное для чего они придуманы, это чтобы у адептов Haskell было постоянное развлечение в виде «напишу-ка я очередную статью о монадах, теперь с картинками!». Нет, ну реально. Ещё ни разу не видел статью о монадах вида «я придумал крутую монаду XYZ, теперь мой код стал понятнее на 50% и короче на 60». Зато, при этом есть куча статей с объяснениями того, что такое монада в Haskell. Разве это не является признаком того, что с практической точки зрения монада — это какой-то странноватый инструмент?

Затем же зачем нужны C, C++, Java, Python и остальные языки программирования.

Как бы… Эмс… То, что существует множество разных языков программирования не наталкивает вас на мысль, что эти языки нужны для решения различных задач? IMHO, тотально глупо считать, что Haskell, Python, Bash и C позволяют одинаково хорошо решать разные задачи.

Есть.
hackage.haskell.org/packages/archive/pkg-list.html


Большинство из этих пакетов — реализации примитивных структур данных, которые почему-то в других сообществах являются самоочевидными. Ну никто в мире Си не гордится тем, что реализовал набор queue-like data structures, даже студенты, которые хотят на халяву зачёт по курсовой получить.

Покажите мне реальные приложения. Я вот когда-то смотрел на darcs и frag, первый меня убил обилием кода на Си, второй ужасными конструкциями с несколькими видами стрелок, за каждой из которых стояла какая-то перестановочная семантика, отслеживание которой по коду в течении сотни строчек надолго привило мне стойкое неприятие Haskell. На Си у Кармака то же самое написано в 10 раз понятнее и лаконичнее. Так зачем тогда городить этот огород со стрелками? Не понятно.

foreign — в книге всего одна глава из 28, и это далеко не все примеры.
небезопасные массивы тоже есть, но в очень малом количестве случаев.


Да, но только это малое количество случаев действительно интересные для реальной жизни программы, вроде программы для распознавания штрих-кодов, не искусственные. Остальное там опять же борьба с ветряными мельницами, imho.

Любой оператор, любого языка программирования суть есть абстракция.

Неа. Если вы знаете Haskell, то должны знать, что такое абстракция. Операторы как раз абстракциями не являются. Потому что операторы — это применение абстракций. А абстракции применять абстракции без наличия интерпретатора не могут. Это ещё со времён дедушки Чёрча известно :) Тоже вначале почитайте книги, желательно, по теории языков программирования.
0
roman_kashitsyn, #
Во многом вы правы. Образцов грамотного применения Haskell не так уж много, а изучающие его в основном ограничиваются теорией. Мне бы тоже хотелось видеть побольше статей о практических применениях этого замечательного языка.
Да и по большому счёту, сложность программы определяется не столько выбором языка, сколько сложностью самой задачи и талантом разработчика.
Другая проблема заключается в том, что за разработик не может выбирать язык исходя только из своей прихоти, нужно учитывать знания и опыт коллег, существующие наработки, условия применения. По своему опыту могу сказать, что на 100 человек коллектива обычно набирается 1-2 человека, хоть как-то знающих Haskell (я уж не говорю об опыте его применения).

Вот и получается, что реально на Haskell пишут либо в узко специализированных компаниях, либо одиночки-энтузиасты.
–2
chemistmail, #
Но я за долгое время так и не смог понять, чем хороши монады. Я понимаю, зачем они нужны в чистом функциональном языке, но я не понимаю, почему это это всё полезно и удобно при условии существования кучи других языков программирования. Я не понимаю, в чём «изящество» и мощь этих механизмов.

Ну если не понимаете, может это вам просто не нужно.
Я тоже много чего не понимаю, и меня это не сильно печалит.

И у меня есть стойкое ощущение, что единственное для чего они придуманы, это чтобы у адептов Haskell было постоянное развлечение в виде «напишу-ка я очередную статью о монадах, теперь с картинками!». Нет, ну реально. Ещё ни разу не видел статью о монадах вида «я придумал крутую монаду XYZ, теперь мой код стал понятнее на 50% и короче на 60». Зато, при этом есть куча статей с объяснениями того, что такое монада в Haskell. Разве это не является признаком того, что с практической точки зрения монада — это какой-то странноватый инструмент?

Ну не знаю, статей на тему монад не писал, да и желания не возникало.
Мониторю www.reddit.com/r/haskell/
Засилья статей на тему монад не замечаю, попадаются иногда, но не до фанатизма.
Да и нормальный это инструмент, если понимаешь что это и как его применять.

Как бы… Эмс… То, что существует множество разных языков программирования не наталкивает вас на мысль, что эти языки нужны для решения различных задач? IMHO, тотально глупо считать, что Haskell, Python, Bash и C позволяют одинаково хорошо решать разные задачи.


Языки это инструменты, если я знаю haskell, erlang и bash, то задачи я буду решать с их помощью.
Даже если я понимаю что в данном случае С подойдет лучше, я его не знаю, пытался, но «не шмогла я не шмогла».

Большинство из этих пакетов — реализации примитивных структур данных, которые почему-то в других сообществах являются самоочевидными. Ну никто в мире Си не гордится тем, что реализовал набор queue-like data structures, даже студенты, которые хотят на халяву зачёт по курсовой получить.

Ну тут и сказать мне не чего.
Про гордость вообще не понятно.

Покажите мне реальные приложения. Я вот когда-то смотрел на darcs и frag, первый меня убил обилием кода на Си, второй ужасными конструкциями с несколькими видами стрелок, за каждой из которых стояла какая-то перестановочная семантика, отслеживание которой по коду в течении сотни строчек надолго привило мне стойкое неприятие Haskell. На Си у Кармака то же самое написано в 10 раз понятнее и лаконичнее. Так зачем тогда городить этот огород со стрелками? Не понятно.

ок.
Использую xmonad в качестве оконного менеджера. Работает, нравится.
Есть свой реальный код в реальной работающей инфраструктуре, работает, всех устраивает.
Пользуюсь облаком selectel, у них тоже что то на haskell.
А насчет понятнее на С, ну ради бога, мне понятнее на haskell.

На тему что такое абстракция спорить не буду, сильно это все вилами на воде писано.
Лопатой можно копать землю, а абстракциями оперировать можно только в уме.
А у каждого он свой, и уникальный.

0
megalol, #
Частных случаев монад много в «реальном мире» — это LINQ, генераторы списков, continuations. Такой частный случай можно сделать на многих языках. Как, на С можно сделать объектную систему без ключевого слова class в языка — так во многих языках с ФВП монады эмулируются в частностях. Делаешь частный случай `return` и `bind` (например `concat. fmap`) — и вот ты написал генератор списков на ФВП. Скорее всего, неюзабельный из-за синтаксиса, но сделать его можно. Я такое даже на Java с анонимными интерфейсами писал.
Итак, частные случаи удобны и полезны, особенно если в языке можно запилить do-нотацию. Хаскель, ко всему этому, дает `Monad m => ...`, то есть функции, которые работают с любыми монадами. Надо это или нет? Наверное, прикольно писать код на одних `forM_` и `repeat`, который бы работал одновременно с IO, ST и какой-нибудь асинхронной remote procedure call монадой. Хотя я не ни разу такой код не видел за пределами модуля `Control.Monad`, потому что у каждой монады есть своя специфика.

Чистоту ради чистоты я не понимаю, и, по-моему, никто не понимает. Взять XMonad. Что такое X в этом названии? Это тип
newtype X a = X (ReaderT XConf (StateT XState IO) a)
Зачем оно нужно? Потому что большинства функций тип
  что-то -> что-то -> X (что-то)
То есть де-факто большая часть функций довольно грязные. Надо ли это? Не знаю и не очень понимаю, чему здесь помогает этот стек трансформеров.
0
chemistmail, #
newtype — тип обертка, по всей видимости объявлен чтоб не писать длинную аннотацию и для объявления instance.
Внутри:
ReaderT XConf (StateT XState IO) a
Стэк монад, внутри есть read-only XConf, конфигурация.
+ XState — состояние, можно читать и писать.

Сразу по дефолту есть instance для следующих классов
(Functor, Monad, MonadIO, MonadState XState, MonadReader XConf, Typeable)

В коде это дает возможность применять следующие функции к объектам типа X
1. Functor
fmap :: (a -> b) -> f a -> f b
2. Monad
bind, return и тд
3. MonadState XState
get -> получить состояние
set -> изменить состояние на новое
+ еще некоторый набор связанных функций.
4. MonadIO
liftIO :: IO a -> m a
т.е. возможность выполнять IO в этой монаде.
5. MonadReader XConf
ask -> считать конфигурацию
+ связанные функции.
6. Typeable
typeOf :: a -> TypeRep
Для типа X имеем подпись типа.

Далее определенны еще несколько instance
7. Applicative
Набор функций рассмотрен в статье
8. (Monoid a) => Monoid (X a)
mempty -> нейтральный элемент
mappend -> комбинирование двух элементов одного типа, результат того же типа
для строки mappend «при» «вет» = «привет»

Все это понятно просто по аннотации типов.
Собственно весь код ниже.
-- | The X monad, 'ReaderT' and 'StateT' transformers over 'IO'
-- encapsulating the window manager configuration and state,
-- respectively.
--
-- Dynamic components may be retrieved with 'get', static components
-- with 'ask'. With newtype deriving we get readers and state monads
-- instantiated on 'XConf' and 'XState' automatically.
--
newtype X a = X (ReaderT XConf (StateT XState IO) a)
    deriving (Functor, Monad, MonadIO, MonadState XState, MonadReader XConf, Typeable)

instance Applicative X where
  pure = return
  (<*>) = ap

instance (Monoid a) => Monoid (X a) where
    mempty  = return mempty
    mappend = liftM2 mappend
+2
Googolplex, #
Вот тип основной операции аппликативного функтора:
(<*>) :: (Applicative f) => f (a -> b) -> f a -> f b

А вот тип основной операции монады (только с другим порядком аргументов):
flip (>>=) :: (Monad f) => (a -> f b) -> f a -> f b

Эти операции похожи, но, очевидно, разные. Вторая операция «мощнее» первой, т.е. <*> можно выразить через >>=, а вот наоборот — нет.

Если чуть подробнее, то первая операция работает только с «чистыми» функциями, т. е. в случае аппликативного функтора вы можете совершать над элементами «контейнера» только операции без побочных эффектов (т.е. операции, которые никогда не могут вернуть сам «контейнер»). Монада же позволяет применять к значениям внутри «контейнера» любые операции, в том числе и с побочными эффектами (т.е. возвращающие «контейнер» того же рода).

Если ещё более подробно, то монада позволяет «схлопывать» два уровня «контейнера», т.е. для монад можно определить функцию
join :: (Monad m) => m (m a) -> m a
Наличие такой функции, в целом, и обеспечивает возможность применять к значениям в «контейнерах» операции с побочными эффектами. А вот для аппликативных функторов такую функцию определить нельзя.

Пример с Maybe здесь наиболее иллюстративен. Так как Maybe является монадой, то вы можете написать такое (здесь все операции возвращают Maybe):
do result1 <- someFailingOperation1
   result2 <- someFailingOperation2
   result3 <- someFailingOperation3 result2
   someFailingOperation4 result1 result3

или, эквивалентно,
someFailingOperation1 >>= \result1 ->
  someFailingOperation2 >>= \result2 ->
  someFailingOperation3 result2 >>= \result3 ->
  someFailingOperation4 result1 result3


Причём за счёт свойств монады Maybe, описанных в статье, такая цепочка операций вернёт Nothing, если хотя бы одна операция вернула Nothing. Заметьте, что здесь мы применяем someFailingOperation{3,4} к результатам предыдущих операций.

Если бы Maybe был бы только аппликативным функтором, но не монадой, то такого мы бы написать не смогли: в случае аппликативных функторов функции внутри контекста не могут возвращать сам аппликативный функтор. Вернее, они могут, но тогда мы получим два уровня вложенности контейнеров, а «схлопнуть» их не получится, что делает невозможным их сколько-нибудь осмысленное применение.
0
jakobz, #
Разница в том, что вычисления в монаде могут зависеть от предыдущих, а в случае аппликативных функторов — нет. В примере первая операция считывает имя файла, а вторая — читает файл с этим именем. Так с аппликативными функторами не выйдет.

Можно попробовать провернуть с аппликативными функторами такое же, как в примерах с числами и функциями: положить в «коробку» функцию, которая возьмет имя файла, и вернет «коробку с именем файла». Но тогда на выходе получится «коробка» внутри другой «коробки». Есть альтернативное определение монад — через функцию, которая из коробки в коробке, делает просто коробку. Там одно к другому сводится.

«Контекст значения», «коробка» или тип данных — очень похож на generic-тип, например List — «коробка» для типов T. Классы типов, т.е. эти все функторы — похожи на generic-интерфейс. Если говорят что список — это монада, то в ООП это что-то вроде «List реализует интерфейс IMonad».
0
chemistmail, #
Компилятор заругает, и будет прав.

>>> :t getLine — смотрим тип для getLine
getLine :: IO String — (f a) Упакованная строка

>>> :t readFile — смотрим тип для readFile
readFile :: FilePath -> IO String — (a -> f b) Функция из строки возвращает упакованное значение

>>> :t (<*>) — смотрим тип для <*>
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
а вот тут и нестыковка
f(a -> b) хотя у нас a -> f b

а вот >>= как раз подходит
>>> :t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b

Вторую фразу не понял.
0
jack128, #
мешает то, что getLine/readFile/putStrLn — это НЕ упакованные функции.
контекст — это generic тип, параметризируемый типом значения. контекст значения и контекст функции — это одно и тоже, ведь функции — это тоже значения.
Just 10 — имеет тип MayBe < int >
Just (+3) имеет тип MayBe<int(int)> в плюсах (или MayBe<Func<int, int>> в шарпе)
+5
impwx, #
Некоторые картинки вызывают ощущение «если бы заранее не знал, то не понял бы». То, что (+3) и 2 в коробке не складываются, а с помощью fmap и «немного магии» вдруг сложились — это жесть.
0
jakobz, #
Ну, например обычный массив — это коробка для значений каких-то типов. Массивы чисел сами по себе не складываются же.
+2
impwx, #
Да, массивы не складываются, но в таком случае как раз и нужно объяснить, что fmap делает для того, чтобы они-таки сложились. Иначе остается белое пятно в понимании базового принципа применения функций и кажется, что эта функция и действительно «магическая».
+5
Googolplex, #
Спасибо большое, замечательный перевод! Особенно порадовала картинка «наблюдающего аппликативного функтора»)

Небольшое замечание:
Функтор — это тип классов.

Type class переводится скорее как «класс типов», тип классов = type of classes же.
0
mojojojo, #
Абсолютно верно. Сам это же замечание хотел оставить. При таком переводе абсолютно неверный смысл приобретает.
0
AveNat, #
Всё именно так, уже исправила.
+6
KeepYourMind, #
Статья красивая, но если ее цель ввести не опытного читателя в заявленную тему, то она не достигнута.
После прочтения статьи я все равно ничего не понял про функторы и монады.
+1
mikhanoid, #
Можете мыслить себе так: это просто такая хрень, которая нужна в чистых функциональных языках для трэдинга ошибок или состояний. Трэдинг — это протаскивание какого-то значения через цепочку или дерево вызовов. Обычно, функторы и монады применяются именно для этого: чтобы не описывать такое протаскивание вручную вводятся специальные абстракции в виде конструкторов типов, которые скрывают всю механику протаскивания у себя внутри.

С не очень большой степенью натяжки, вы можете считать код блока в своём любимом императивном языке программирования монадой. А вычисление значения с использованием исключений — функтором.
0
KeepYourMind, #
Ну хоть чуток стало яснее :-)
+1
roman_kashitsyn, #
Это поразительное свойство абстракций. Когда они усваиваются мозгом, то кажутся чрезвычайно простыми, можешь их объяснить на пальцах (как, например в статье). Но на усвоение абстракции может уйти и полгода, и редко какие-либо объяснения могут прояснить суть дела, как-то само со временем приходит.
+1
mikhanoid, #
Да ладно? В статье объяснение абсолютно фиговое. Ибо, у любого новичка возникнет вопрос: WTF!? зачем надо 2 засовывать в коробку, чтобы сложить с 3. Не правильно все эти статьи пишутся, по корявой схеме. Между тем, уже четыре года существует монументальный учебник DCPL, где всё очень качественно разложено по полочкам. Почему никто из профессиональных авторов статей о монадах на эту книгу не ссылается?
0
Qrilka, #
District of Columbia Public Library?
0
roman_kashitsyn, #
Думаю, скорее Design Concepts in Programming Languages.
0
Vladimir_Izotov, #
Как я уложил монады у себя в голове:

Монада — это виртуальная машина которая исполняет ваш код. Не зря ведь говорится, что код исполняется «в монаде». Она может быть написана самостоятельно или взята из библиотеки.
Признаком того, что сейчас у нас работает именно эта монада, является тот самый контейнерный тип данных, который ей соответствует. Поэтому вызовы >>= и Return (благодаря полиморфизму) будут использовать реализацию из неё.
Получив вызов >>=, монада определяет, каким образом исполнять следующую команду (функцию), т.е. работает в точности как процессор в компьютере.
Кроме того, в этом контейнере хранится служебная информация для самой виртуальной машины (вроде состояния в монаде State).
Проверка типов самого языка контролирует корректный вход и выход из монады. Вот и всё.
+3
chersanya, #
А есть здесь кто-нибудь, кто реально использует Haskell в достаточно больших проектах? Я писал на нём достаточно небольшие программы, и пока нет проблем с производительностью — всё отлично. Однако, иногда проявляется слишком медленное выполнение, например из-за ленивости вычислений, или ещё по какой-либо причине. И в таких случаях в силу опять же ленивых вычислений бывает очень сложно найти реальную причину этого, локализовать её, и затем исправить. Как вообще решаются такие проблемы в больших программах?
0
Qrilka, #
По поводу утечек из-за ленивости есть вот такая статья — blog.ezyang.com/2011/06/pinpointing-space-leaks-in-big-programs/
По сути же это вопрос опыта, понимать где ленивость может выйти «боком».
+2
knsd, #
Активно используется в Селектеле, в том числе и в больших проектах.

Общие принципы на мой взгляд:
  • Используйте строгие структуры, например Control.Monad.State.Strict вместо Control.Monad.State.Lazy
  • Используйте deepseq
  • Используйте BangPatterns
  • Определяя свои типы, так же определяйте строгие поля, {-# UNPACK #-} сэкономит вам память, в GHC 7.8 можно будет распаковывать даже полиморфные поля
  • Используйте более производительные (в общем случае) контейнерные типы, например Data.Vector вместо Data.List, Data.HashMap.Strict вместо Data.Map, Text вместо String и ByteString когда вас интересует только массив байтов. String лучше не использовать в принципе


Если вы подозреваете утечки, их часто можно найти используя ghc-heap-view.
0
chemistmail, #
+ туда же
io-streams
conduits
pipes
Выбрать что больше нравится.
0
Amomum, #
А вы не могли бы пояснить, что в статье понимается под «контекстом» и «упаковкой»?
0
AveNat, #
На сколько я понимаю, контекст — это ситуация вокруг значения. Допустим, в ответ на запрос записи из БД может прийти правильный ответ (если такая запись имеется), а может прийти код ошибки/None/что-нибудь в этом роде. Упакованные (wrapped) значения содержат в себе возможность адекватно отреагировать на оба случая. Т.е. способны подстраивать своё поведение под перемены «окружающей среды» (контекста).
–1
Amomum, #
Но вы не уверены? О_о
0
AveNat, #
Просто всегда допускаю возможность того, что могу недопонимать каких-то тонкостей. Я же не Haskell-гуру (пока).
+1
Vladimir_Izotov, #
Думаю, что контейнерный тип данных.
Вроде как в java можно обычную переменную типа int обернуть в специальный класс, в экземпляре которого она будет в виде поля, и дальше работать уже с этим экземпляром.
0
leventov, #
Не покидает ощущение что уже видел на Хабре очень похожий пост про Хаскель для начинающих с красивыми-красивыми картинками. + Стиль книги «Изучи Хаскель во имя добра». Особый жанр!
0
AveNat, #
Когда шерстила Хабр на возможность дублей статьи, то ничего похожего не нашла. Будет здорово, если вы найдёте и дадите ссылку!
0
nwalker, #
Я бы предпочел толковое описание использования аппликативных функторов, в частности, когда и какой код с их помощью можно сделать лаконичнее.
0
KonstantinSolomatov, #
Да много какой, например, UI-ный: www.konstantinsolomatov.com/monads-and-applicatives-in-ui-programming
+1
chemistmail, #
Примеры кода подойдут?
Легко ищутся по запросу типа
instance FromJSON

для примера
hackage.haskell.org/packages/archive/github/0.7.0/doc/html/src/Github-Data.html
0
alhimik45, #
Спасибо! Прекрасная статья. После неё прочитал вот эту. Почти всё понял, но есть пара вопросов:
1. Я так понял, что Map, описанный а той статье и есть хаскелевый fmap?
2. Можно ли достать значение из коробки ( Just 5 => 5)? И что случиться, если там будет Nothing?
+3
Googolplex, #
1. Почти. В той статье Map объявлен только для Maybe, а хаскеллевский fmap работает с любым функтором.
2. Это зависит от того, что вы понимаете под «достать» и от вида «коробки». Если вы имеете в виду, можно ли написать функцию вида Monad m => m a -> a, то в общем случае нельзя. Как вы справедливо заметили, из Nothing доставать нечего. А из IO тем более просто так ничего достать не получится, сайд-эффекты во все поля.

Однако для конкретных монад/функторов вполне могут быть доступны возможности извлечь значение из «коробки» либо выполнить какие-нибудь действия, если это невозможно. В любом случае этот процесс «доставания» сведётся к pattern matching, потому что в конечном итоге любой тип данных в хаскелле есть алгебраический тип данных, а для них естественным способом «деконструкции» является как раз паттерн матчинг. Например, ответ на ваш вопрос про Maybe может выглядеть вот так:
someFunction m = case m of
  Just v -> ...  -- Maybe и в самом деле содержит значение; в этой ветке оно теперь доступно под именем v
  Nothing -> ...  -- Значения нет, делаем что-то другое, например, используем дефолтное


Однако сила монад и функторов, на самом деле, не непосредственно в возможности извлечь оттуда значение, а в наличии огромного количества комбинаторов. Например, для Maybe существует комбинатор maybe :: b -> (a -> b) -> Maybe a -> b, который определён примерно так:
maybe def f m = case m of
  Just v -> f v
  Nothing -> def

С его помощью очень удобно обрабатывать Maybe-значения.

Есть общие комбинаторы, вроде >>= и <*>, есть и частные, типа maybe. Они позволяют очень точно, чётко и ясно выражать различные действия внутри монад и функторов.

Любую другую монаду/функтор (кроме IO, но это, понятно, отдельная история), вообще говоря, можно «разобрать» на части с помощью паттернматчинга. Конечно, если автор библиотеки не стал экспортировать конструкторы своей монады/функтора, то пользователи этой библиотеки не смогут её «разобрать» непосредственно, но в таком случае автор, как правило, предоставляет другие возможности получить значение из «контейнера», если это вообще можно сделать в принципе.
0
alhimik45, #
Спасибо. Кажется, понял.
0
AveNat, #
Пожалуйста, рада, что вам понравилось!

*тут были ответы на вопросы, удалила из-за их малоинформативности. Выше всё прекрасно расписали*
0
safinaskar, #
Надо было в заголовке или первых строках сказать, что речь про Haskell
0
senia, #
Скорее на примере хаскеля. Ему все это принадлежит не монопольно.
0
safinaskar, #
В «Learn You a Haskell» много картинок не по теме, в отличие от вашей статьи

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.