swift
December 2, 2020

Функторы и монады в Swift

собственно, аппликативный функтор или просто Applicative

Каждый разработчик, копнув чуть дальше в области функционального реактивного программирования, рано или поздно сталкивается с такими понятиями, как функтор или монада. Поначалу они могут показаться довольно страшными, но на самом деле ничего страшного или сложного в них нет.

Попробуем разобраться в этих, довольно далеких от императивного программирования, концепциях.

Если текст покажется вам слишком сложным, вернитесь к нему еще раз после прочтения статьи по хаскелю, которую я приложу в самом конце статьи.

Значения в контексте

Попробуем сначала разобраться что такое контекст. Допустим у нас есть некоторая простая переменная, например var number = 3. Теперь давайте упакуем это значение в контекст. Представим коробку, в которую мы кладем это значение. Коробка по сути и будет являться простой концепцией контекста.

Такая коробка может иметь значение, а может и вовсе быть пустой (например как массив значений, или реальная коробка из реального мира). Оперировать с таким значением напрямую мы не можем, поскольку оно упаковано в контексте, но можно попробовать каким-либо образом "распаковать" его, если контейнер не пуст.

Значение в контексте, или же просто упакованное значение, это одно и то же.

Внимательный читатель может заметить, что прообразом подобной коробки может являться тип Optional в языке Swift, поскольку Optional может как иметь некоторое значение, так и быть nil. Для наших экспериментов создадим наш собственный Optional в виде дженерика Box<T>, имеющего два кейса:

Значение, упакованное в контексте

Ничего сложного тут нет, даже если вы слабо знакомы с синтаксисом Swift. Перечисления в Swift объявляются в помощью ключевого слова enum и могут иметь различные кейсы перечисления, определяемые с помощью слова case. В данном случае мы можем либо вернуть некоторое значение типа T либо ничего. Переходим к функторам.

Функтор

Функтор – это такой тип данных, для которого определена функция map.

Теперь у нас есть значение в контексте (например целое число 7), и мы хотим добавить к нему 3. Но поскольку значение упаковано в контекст, мы не можем просто добавить к нему 3.

Box(7) + 3 -> Type Error.

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

функция add для значений целого типа

Здесь мы определили функцию add, принимающую целое значение в качестве параметра, выполняющую некоторые вычисления и возвращающую Box типа Int. Давайте перепишем его и сделаем более абстрактным (generic). Здесь на сцену выходит функция map. Такая функция принимает в качестве аргумента другую функцию с сигнатурой (T) -> U, становясь по сути независимой от типа.

функция map

Что мы в итоге имеем:

  • функция map принимающая функцию (T) -> U в качестве единственного аргумента и возвращающая Box<U>
  • если внутри контекста есть значение, мы применяем к нему функцию f, получаем в результате значение типа U, оборачиваем это значение обратно в контекст Box<U> и возвращаем его
  • если внутри контекста нет значения, возвращаем empty

Используя функцию map можно применять любую функцию к значению, заключенному в контекст Box.

Функтор применяет функцию к значению, заключенному в контексте

Другими словами функтор – это тип данных, для которого определена функция map – функция которая знает как применить другую функцию к значению в контексте.

Аппликативный функтор

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

Итак, мы уже знаем что такое значение в контексте, и как применять к этим значениям функции с помощью функторов. Но допустим, теперь и функция тоже упакована в контекст! И как нам применить эту функцию к значению в контексте? Нужна немного более мощная вещь чем map. Расширим тип Box с помощью функции apply, которая будет переключаться между функцией в контексте и значением.

функция apply

Обратим внимание на первый some case. Вложенный switch ничего не напоминает? По сути, это тот же самый map, реализованный чуть выше. Давайте немного отрефакторим код и заменим вложенный switch на map.

apply реализованный через map

То есть, apply возвращает некоторое значение, помещенное в контекст только в том случае если есть некоторая функция внутри Box, и некоторое значение внутри Box. В иных случаях, возвращается empty. Теперь определим что такое аппликативный функтор.

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

Другими словами, аппликативный функтор – это тип, который знает как применить функцию помещенную в контекст, к значению помещенному в контекст.

Монада

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

Монада применяет функцию, помещенную в контекст и возвращающую некоторое значение (также помещенное в контекст) к значению из контекста.

Звучит вообще говоря как-то так себе, правда? Рассмотрим на примере. Определим функцию flatMap для Box и посмотрим как работают монады.

функция flatMap

Аргумент функции flatMap возвращает Box<U>, а не просто тип U как например функция apply в аппликативном функторе.

Монады применяют функцию, которая возвращает упакованное в контекст значение, к упакованному в контекст значению.

Заключение

Если вы ничего не поняли, не беда. Все таки функциональное программирование вещь не из простых. Чтобы окончательно разобраться в ситуации, рекомендую изучить следующие ссылки:

Функторы и монады в картинках

Монады за 15 минут

Англоязычный сайт по хаскелю

Попробовать Swift можно, например тут.

Статья подготовлена и написана для канала Hello World.