Я с нетерпением ждал этого! Если вы попытаетесь лучше понять функциональное программирование, вы в конце концов столкнетесь с загадочным термином Монада.

В этой записи блога мы рассмотрим странные термины из математики и узнаем, как они полезны, когда мы пишем программное обеспечение для решения нематематических задач.

Проклятие монад состоит в том, что как только кто-то узнает, что такое монады и как их использовать, он теряет способность объяснять их другим людям. (Дуглас Крокфорд — Monads and Gonads talk)

Прежде чем мы перейдем к монадам, мы рассмотрим (эндо-) функторы и моноиды. Не волнуйтесь, это полезные концепции со странными названиями. Возможно, вы даже знакомы с некоторыми из их реализаций.

На самом деле важно не знать, что эти абстракции являются функторами или монадами, а скорее понимать, что делает плоская карта в необязательном Java или .then в промисе JavaScript. Как только мы поймем, как использовать функторы, моноиды и монады, мы сможем писать более простой и лаконичный код.

Функторы

Functor — это тип данных, который сохраняет структуру и реализует функцию карты. Сохранение структуры просто означает, что вы можете поместить значение внутрь, и сама структура его не изменит. Map (или fmap) — это функция, которая принимает функцию в качестве аргумента и применяет эту функцию к значениям в структуре, с которой она работает.

Это объяснение может вам помочь, а может и не помочь. Давайте сделаем его менее абстрактным. Наиболее распространенным примером функтора, вероятно, является массив. Массив — это оболочка для множества значений. Когда вы вызываете для него карту, она возвращает массив, состоящий из результатов значений, примененных к функции, которая была передана в карту.

Другим примером Functor является необязательный тип в Java. Здесь карта позволяет вам применять вычисления, не проверяя, пуст ли необязательный параметр. По сути, если вы сопоставляете необязательный элемент, происходит следующее. Если есть значение, оно передается в функцию, с которой вызывается карта. Если необязательный параметр пуст, карта возвращает пустой необязательный элемент.

Сопоставление с необязательным параметром позволяет нам безопасно изменять значение в необязательном без необходимости использовать операторы if. В Spring контроллер может работать даже с необязательными параметрами и будет возвращать ошибку 404, если необязательный элемент пуст. Если бы мы не использовали Functor здесь, нам пришлось бы дважды разветвляться, один раз в сервисе, чтобы убедиться, что мы пытаемся изменить значение только в том случае, если репозиторий вернул значение, а также в контроллере, чтобы убедиться, что возвращается Статус 404, если значение не было возвращено. Functor элегантно скрывает эту сложность и делает код более кратким, читабельным и удобным в сопровождении.

Если подумать, это точно такое же поведение для массива. Если вы сопоставляете пустой массив, карта возвращает пустой массив.

Endofunctor — это просто Functor, в котором map не меняет основную структуру. Давайте снова возьмем массив в качестве примера. Если вы сопоставите массив, вы получите другой массив, сопоставление массива не вернет объект или необязательный элемент, он всегда будет возвращать массив. Вот что значит «не изменять структуру».

моноиды

Моноид — это операция, обладающая двумя свойствами: ассоциативностью и идентичностью. Давайте рассмотрим 3 примера, чтобы понять, что это значит.

Если операция ассоциативна, ее можно разбить на группы, и порядок выполнения каждой группы не влияет на результат. Например. при сложении значений, если я хочу вычислить сумму 1,2 и 3, не имеет значения, вычисляю ли я сначала 1+2, а затем добавляю 3, или сначала вычисляю 2+3, а затем эту сумму до 1.

Операция выполняет свойство идентичности, если мы можем найти значение идентичности для операции, которое не повлияет на результат операции. Это означает, что если вы выполните операцию с идентификатором и x, результатом будет x, независимо от того, что такое x. Для сложения значение тождества равно 0. 0 + x = x.

Но почему?

Помните, что функциональное программирование направлено на написание декларативного кода вместо императивного. Моноиды помогают нам в этом.

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

Мы узнали, что сложение ассоциативно, поэтому нам просто нужно добавить каждое число к предыдущей сумме, начиная с нашей идентичности.

Монады

Теперь, когда мы рассмотрели эндофункторы и моноиды, у вас должно быть достаточно хорошее представление о том, что такое монада. Я просто шучу. Если да, то я не знаю, как вы туда попали.

Проще говоря, я бы сказал, что монада — это вложенный функтор, который реализует плоскую карту.

Давайте снова воспользуемся примерами, чтобы сделать это более конкретным. Самая близкая к монаде вещь, которую я постоянно использую, — это обещание JavaScript.

Пожалуйста, не кодируйте таким образом, временные переменные бесполезны, а обработка ошибок отсутствует. Это просто для демонстрационных целей. Обратите внимание, что когда мы вызываем .then для промиса, мы можем передать функцию, которая будет вызываться со значением внутри промиса, а затем обернуть результат в другой промис. Это звучит знакомо? Так и должно быть, это именно то, что карта делает для функтора.

Что произойдет, если мы передадим функцию, возвращающую Promise, в Promise.then? И для целей сравнения, что произойдет, если мы передадим функцию, которая возвращает array.map?

Обратите внимание, что у нас есть вложенный массив с сигнатурой типа Array‹Array‹number››, но у нас нет вложенного Promise: Promise‹Response› вместо Promise‹Promise‹Response››. Promise.then сглаживает вложенные обещания. Он реализует как карту, так и плоскую карту.

Давайте рассмотрим еще один пример из Java’sOptional. Как бы мы поступили с вложенными опционами?

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

Монады не обязательно должны вкладываться в один и тот же тип, более сложные монады, такие как Result, Does или Haskell IO Monad, вложены в разные типы, но чтобы не запутать вас, я не буду вдаваться в это.

В монадах, моноидах и функторах есть еще много математики, в которую я не хочу вникать, да и не до конца понимаю. Это не должно мешать нам использовать эти абстракции.

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