Монады используются для компоновки функции (function composition) и избавления от связанного с этим утомительного однообразия. После семи лет программирования на Go необходимость повторять if err != nil превращается в рутину. Каждый раз, когда я пишу эту строку, я благодарю Gopher’ов за читабельный язык с прекрасным инструментарием, но в то же время проклинаю за то, что чувствую себя наказанным Бартом Симпсоном.  Подозреваю, что это чувство разделяют многие, но Монады нужны не только для того, чтобы спрятать от нас обработки ошибок, но и для списковых включений, и для согласованности, и для других задач. Эрик Мейер (Erik Meijer) в своей статье Introduction to Functional Programming Course on Edx просит больше не писать о монадах, потому что о них и так уже очень много сказано. Помимо чтения этой статьи я рекомендую посмотреть серию видео Бартоша Милевски о теории категорий, которая завершается выступлением с лучшим объяснением монад, какое мне только встречалось.  Дальше не читайте! Ну ладно (вздох)… Только помните: я вас предупреждал.
До монад нужно разобраться с функторами. Функтор — это надкласс (superclass) монады, т. е. все монады — это функторы. Я буду использовать функторы в дальнейшем объяснении сути монад, так что не пропускайте этот раздел. Функтор можно считать контейнером, содержащим один тип элементов. Например: Программистам, владеющим не Go, а другими языками: в Go нет алгебраических типов данных или объединённых типов (union types). Это значит, что вместо возврата функцией значения или ошибки здесь возвращается значение и ошибка, и одно из них обычно — nil. Иногда мы нарушаем соглашение и возвращаем значение и ошибку, оба не nil, чтобы просто попытаться запутать друг друга. Или поразвлечься.  Самый популярный способ получить объединённые типы в Go: нужно иметь интерфейс (абстрактный класс) и переключатель типа (type switch) в типе интерфейса. Ещё один критерий, без которого контейнер не считается функтором, — необходимость реализации функции fmap для этого типа контейнера. Функция fmap применяет функцию к каждому элементу в контейнере без изменения контейнера или структуры. Использование функции map в качестве слайса — классический пример, который вы могли встречать в mapreduce в Hadoop, Python, Ruby и почти любом другом языке: Мы также можем реализовать fmap для дерева: Или для канала: Или для указателя: Или для функции: Или для функции, возвращающей ошибку: Все эти контейнеры с соответствующими реализациями fmap — функторы. Теперь мы знаем, что функтор — это абстрактное название контейнера и что мы можем применять функцию к элементам внутри контейнера. Теперь перейдём к сути статьи — к абстрактной концепции монады. Монада — это просто «украшенный» тип. Хм, полагаю, понятнее не стало, слишком абстрактно. Это типичная проблема всех объяснений сути монад. Это как попытка растолковать, что такое побочный эффект: описание будет слишком общим. Так что давайте лучше разберёмся с причиной абстрактности монады. Причина в том, чтобы компоновать функции, которые возвращают эти украшенные типы.  Начнём с обычной компоновки функций, без украшенных типов. В этом примере мы скомпонуем функции f и g и вернём функцию, берущую входные данные, которые ожидаются f, и возвращающую выходные данные из g: Очевидно, что это будет работать только в том случае, если результирующий тип f соответствует входному типу g. Другая версия: компоновка функций, возвращающих ошибки. Теперь попробуем абстрагировать эту ошибку в виде украшения (embellishment) M и посмотреть, что останется: Нам нужно вернуть функцию, берущую A в качестве входного параметра, так что начнём с объявления возвращающей функции. Раз у нас есть A, мы можем вызвать f и получить значение mb типа M<b>, но что дальше? Мы не достигли цели, поскольку получилось слишком абстрактно. Я хочу сказать, что у нас есть mb, только что с ним делать? Когда мы знали, что это ошибка, то могли проверить её, а теперь не можем, потому что она слишком абстрактна. Но… если мы знаем, что наше украшение M — также функтор, то можем применить к нему fmap: Функция g, к которой мы хотим применить fmap, не возвращает простой тип вроде C, она возвращает M<C>. К счастью, для fmap это не проблема, но зато меняется сигнатура типа: Теперь у нас есть значение mmc типа M<M<C>>: Мы хотим перейти от M<M<C>> к M<C>. Для этого нужно, чтобы наше украшение M не просто было функтором, а имело и другое свойство. Это свойство — функция join, которая определена для каждой монады, как fmap была определена для каждого функтора. Теперь мы можем написать: Это означает, что если при украшении определены fmap и join, то мы можем скомпоновать две функции, возвращающие украшенные типы. Иными словами, необходимо определить эти две функции, чтобы тип был монадой. Монады — это функторы, так что нам не нужно опять определять для них fmap. Нужно определить лишь join. Теперь определим join: Простейший способ начать — применить join к слайсам. Эта функция просто конкатенирует все слайсы. Давайте посмотрим, зачем нам снова понадобилась join, но в этот раз сосредоточимся на слайсах. Вот функция compose для них: Если передать a в f, то получим bs типа []B. Теперь можем применить fmap к []B с g, что даст нам значение типа [][]C, а не []C: Вот для чего нам нужна join. Мы переходим от css к cs, или от [][]C к []C. Давайте рассмотрим более конкретный пример. Если мы заменяем типы: Тогда наша функция становится: Теперь можем использовать их в нашем примере: Получается слайс нашей первой монады. Любопытно, что именно так работают списковые выражения в Haskell: Но вы могли узнать их на примере Python: Мы также можем определить join для функций, возвращающих значение и ошибку. Для этого сначала нужно опять вернуться к функции fmap из-за некоторой идиосинкразии в Go: Мы знаем, что наша функция компоновки собирается вызвать fmap с функцией f, которая тоже возвращает ошибку. В результате сигнатура fmap выглядит так: К сожалению, кортежи в Go не относятся к объектам первого уровня, поэтому мы не можем просто написать: Есть несколько путей обхода этого затруднения. Я предпочитаю функцию, потому что функции, которые возвращают кортежи, — это объекты первого уровня: Теперь с помощью нашего ухищрения можем определить fmap для функций, которые возвращают значение и ошибку: Это возвращает нас к основному пункту: функции join применительно к (func() (C, error), error). Решение простое и выполняет для нас одну из проверок ошибки. Теперь можем использовать функцию compose, поскольку уже определили join и fmap: В результате нам нужно выполнять меньше проверок ошибок, потому что монада делает это за нас в фоновом режиме с помощью функции join. Вот ещё один пример, когда я чувствую себя Бартом Симпсоном: Технически compose может взять в качестве параметров более двух функций. Поэтому соберём все вышеупомянутые функции в один вызов и перепишем наш пример: Существует много других монад. Представьте две функции, возвращающие один и тот же тип украшения, которые вы хотите скомпоновать. Разберём ещё один пример. Можно определить join для каналов. Здесь у нас канал in, который снабжает нас каналами типа T. Сначала создадим канал out, запустим горутину для обслуживания канала, а затем вернём её. Внутри горутины запустим новые горутины для каждого из каналов, читающих из in. Эти горутины шлют в out свои входящие события, объединяя входные данные в один поток. Наконец, с помощью группы ожидания (wait group) удостоверимся в закрытии канала out, получив все входные данные. Иными словами, мы читаем все каналы T из in и передаём их в канал out. Для программистов не на Go: мне нужно передать c в качестве параметра во внутреннюю горутину, потому что c — единственная переменная, берущая значение каждого элемента в канале. Это значит, что если вместо создания копии значения посредством передачи его в качестве параметра мы просто используем его внутри замыкания, то, вероятно, сможем читать только из самого свежего канала. Это распространённая среди Go-программистов ошибка. Мы можем определить функцию compose применительно к функции, возвращающей каналы. А из-за способа реализации join мы получаем согласованность почти даром. Это объяснение монад сделано практически на пальцах, и, чтобы воспринималось легче, я намеренно опустил многие вещи. Но есть ещё кое-что, о чём я хочу рассказать. Технически наша функция компоновки, определённая в предыдущей главе, называется Kleisli Arrow. Когда люди говорят о монадах, они редко упоминают Kleisli Arrow, а для меня это стало ключом к пониманию сути монад. Если повезёт, то вам будут объяснять суть с помощью fmap и join, но если вы невезучие, как я, то вам объяснят с помощью функции bind. Почему? Потому что bind — это такая функция в Haskell, которую вам нужно реализовать для своего типа, если хотите, чтобы он считался монадой. Повторим нашу реализацию функции compose: Если бы была реализована функция bind, то мы могли бы просто вызвать её вместо fmap и join. Это означает, что bind(mb, g) = join(fmap(g, mb)). Роль функции bind для списков будут выполнять в зависимости от языка concatMap или flatMap. Я обнаружил, что в Go для меня стало стираться различие между bind и Kleisli Arrow. Go возвращает ошибку в кортеже, но кортеж — не объект первого уровня. Например, этот код не будет скомпилирован, потому что вы не можете посредством инлайнинга передавать результаты f в g: Придётся написать так: Или сделать так, чтобы g принимал функцию в качестве входных данных, потому что функции – это объекты первого уровня. Но это означает, что нашу функцию bind: определённую для ошибок: будет неприятно использовать, пока мы не преобразуем этот кортеж в функцию: Если присмотреться внимательно, то можно увидеть, что наш возвращаемый кортеж — тоже функция: И если присмотреться ещё раз, то окажется, что это наша функция compose, в которой f просто получает нулевые параметры: Тадам! Мы получили свою Kleisli Arrow, просто несколько раз внимательно присмотревшись. Монады с помощью украшенных типов скрывают от нас рутинную логику компоновки функций, чтобы мы могли чувствовать себя не наказанным Бартом Симпсоном, а катающимся на скейте и метко кидающим мячик.  Если хотите попробовать в Go монады и другие концепции функционального программирования, можете делать это с помощью моего генератора кода GoDerive. Предупреждение. Одна из ключевых концепций функционального программирования — неизменяемость. Это не только упрощает работу программ, но и позволяет оптимизировать компиляцию. В Go для эмуляции неизменяемости вам придётся копировать много структур, что снизит производительность. Функциональным языкам это сходит с рук, потому что они могут полагаться на неизменяемость и всегда ссылаются на старые значения, а не копируют их снова. Если вы действительно хотите заняться функциональным программированием, то обратите внимание на Elm. Это статически типизированный функциональный язык программирования для фронтенд-разработки. Для функционального языка он прост в изучении, как Go прост для императивного. Я за день написал руководство и тем же вечером смог начать продуктивно работать. Создатель языка постарался сделать его изучение простым, даже исключив необходимость разбираться с монадами. Лично мне нравится писать фронтенд на Elm в сочетании с бэкендом на Go. А если оба языка уже вам наскучили — то не переживайте, впереди ещё много интересного, вас ждёт Haskell. ¯\_(ツ)_/¯