Функторы и монады в Swift
Каждый разработчик, копнув чуть дальше в области функционального реактивного программирования, рано или поздно сталкивается с такими понятиями, как функтор или монада. Поначалу они могут показаться довольно страшными, но на самом деле ничего страшного или сложного в них нет.
Попробуем разобраться в этих, довольно далеких от императивного программирования, концепциях.
Если текст покажется вам слишком сложным, вернитесь к нему еще раз после прочтения статьи по хаскелю, которую я приложу в самом конце статьи.
Значения в контексте
Попробуем сначала разобраться что такое контекст. Допустим у нас есть некоторая простая переменная, например 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
, принимающую целое значение в качестве параметра, выполняющую некоторые вычисления и возвращающую Box
типа Int
. Давайте перепишем его и сделаем более абстрактным (generic). Здесь на сцену выходит функция map
. Такая функция принимает в качестве аргумента другую функцию с сигнатурой (T) -> U, становясь по сути независимой от типа.
Что мы в итоге имеем:
- функция map принимающая функцию (T) -> U в качестве единственного аргумента и возвращающая
Box<U>
- если внутри контекста есть значение, мы применяем к нему функцию
f
, получаем в результате значение типаU
, оборачиваем это значение обратно в контекстBox<U>
и возвращаем его - если внутри контекста нет значения, возвращаем
empty
Используя функцию map
можно применять любую функцию к значению, заключенному в контекст Box
.
Функтор применяет функцию к значению, заключенному в контексте
Другими словами функтор – это тип данных, для которого определена функция map
– функция которая знает как применить другую функцию к значению в контексте.
Аппликативный функтор
Если пока что все понятно, приступим к аппликативному функтору. Если нет, вернитесь снова к предыдущей главе про функтор, сделайте себе любимый кофе и возвращайтесь к этой главе попозже.
Итак, мы уже знаем что такое значение в контексте, и как применять к этим значениям функции с помощью функторов. Но допустим, теперь и функция тоже упакована в контекст! И как нам применить эту функцию к значению в контексте? Нужна немного более мощная вещь чем map
. Расширим тип Box
с помощью функции apply, которая будет переключаться между функцией в контексте и значением.
Обратим внимание на первый some case. Вложенный switch
ничего не напоминает? По сути, это тот же самый map
, реализованный чуть выше. Давайте немного отрефакторим код и заменим вложенный switch
на map
.
То есть, apply
возвращает некоторое значение, помещенное в контекст только в том случае если есть некоторая функция внутри Box
, и некоторое значение внутри Box
. В иных случаях, возвращается empty
. Теперь определим что такое аппликативный функтор.
Аппликативный функтор применяет функцию из контекста к значению из контекста.
Другими словами, аппликативный функтор – это тип, который знает как применить функцию помещенную в контекст, к значению помещенному в контекст.
Монада
Наконец то. Собственно, это то ради чего мы тут и собрались. Теперь возможно мемы на профункторе станут чуточку понятнее.
Монада применяет функцию, помещенную в контекст и возвращающую некоторое значение (также помещенное в контекст) к значению из контекста.
Звучит вообще говоря как-то так себе, правда? Рассмотрим на примере. Определим функцию flatMap
для Box
и посмотрим как работают монады.
Аргумент функции flatMap
возвращает Box<U>
, а не просто тип U
как например функция apply
в аппликативном функторе.
Монады применяют функцию, которая возвращает упакованное в контекст значение, к упакованному в контекст значению.
Заключение
Если вы ничего не поняли, не беда. Все таки функциональное программирование вещь не из простых. Чтобы окончательно разобраться в ситуации, рекомендую изучить следующие ссылки:
Попробовать Swift можно, например тут.
Статья подготовлена и написана для канала Hello World.