Вам любопытны монады? 🤔 Или, может быть, вы еще дальше по кроличьей 🐰 норе, изучаете аппликативы? «Функтор» звучит для вас чуждо 👽?

Не стоит беспокоиться! 😃

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

Функтор - это простейший паттерн, поэтому имеет смысл начать с него. По мере того, как вы будете работать над монадой, вы увидите, что функтор является основой для аппликативного функтора, который является основой для монады.

⚠️ Если у вас нет анимации, не волнуйтесь. Просто сосредоточьтесь на примерах кода и пояснениях. Анимации нужны для некоторой визуальной интуиции. 👍

Что такое функтор?

Функтор - это список.

map(function)(list) = [function(element) | element <- list]
add1(input) = input + 1
map(add1)([1,2,3])
-- [2,3,4]

Функтор списка принимает функцию и применяет ее к каждому элементу внутри списка. Функция не должна учитывать список или беспокоиться о нем - map обрабатывает список.

Функтор - это функция.

map(function1)(function2)(input) = function1(function2(input))

sub2(input) = input - 2
map(add1)(sub2)(1)
-- 0

Функтор функции объединяет или склеивает две функции вместе, возвращая новую функцию. Эта новая функция направляет свой ввод через две функции, возвращая вывод первой функции.

Функтор - это обещание.

map(function)(promise) = fmap(function)(promise)
promise <- async(return 11)
wait(map(sub2)(promise))
-- 9

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

Функтор - это эффект, для которого вы законно определяете map.

map
  ::       (input -> output) -- Takes a function.
  -> effect(input          ) -- Takes input inside an effect.
  -> effect(         output) -- Returns output inside an effect.

В общем, функтор просто map определен для некоторого эффекта. Функция map

  • принимает функцию input -> output,
  • принимает входные данные для этой функции, застрявшей внутри эффекта effect(input),
  • передает input функции,
  • собирает output функции,
  • и возвращает output, застрявший внутри эффекта effect(output).

Подождите, что вы имеете в виду под «законным определением»?

Определение map должно подчиняться законам функторов.

Сколько существует законов функторов?

Ваше map определение должно подчиняться двум законам.

Хорошо, а какой первый закон?

Если вы передаете идентификатор map, результат map должен совпадать с его входом.

list = [1,2,3]
identity(input) = input
list == map(identity)(list)
-- True

Хм, а какой второй и последний закон?

Если вы составляете две или более функций и передаете их map, результат map должен совпадать с выходом составления нескольких вызовов map вместе - по одному для каждой функции.

times3(input) = input * 3
composition(input) = add1(sub2(times3(input)))
composition(1)
-- 2
list = [1,2,3]
map(composition)(list) == map(add1)(map(sub2)(map(times3)(list)))
-- True

Один вызов map с использованием composition и list равнялся трем вызовам map с использованием add1, sub2 и times3 и list.

Итак, функтор - это интерфейс?

да. Если вы определяете законную версию map для некоторого эффекта, это экземпляр функтора или mappable. map - это перегруженная функция, которая определяется по-разному для каждого типа эффекта, для которого вы ее определяете.

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

Что вы имеете в виду под эффектом?

Список / массив - это эффект. Кортеж / структура / запись - это эффект. Future / Promise - это эффект. Функция - это эффект. Дерево - это эффект. HashMap / Hash / Dictionary - это эффект. Может быть / Необязательный / Обнуляемый - это эффект. Либо (или более специализированная попытка) - это эффект. Эффектов много. Вот некоторые из них.

Спросите, что такое эффект, и вы увидите или услышите «контекст», «функциональный контекст», «контейнер», «контейнер и полезную нагрузку», «оболочку», «коробку», «вычисление», «неоднозначное вычисление», «структуры». »,« Структуры данных »,« недетерминизм »,« недетерминированный »,« конструкторы типов »,« побочные эффекты »,« явный побочный эффект »,« нечистый »и / или« не чистый ». Уф.

Определение эффекта - это «сила, приводящая к результату». Это верно для всего, что я видел, называемого эффектом. У них есть сила или возможность добиться результата, но не всегда. Например, пустой список / массив или ничего / none / null для возможно / optional / nullable.

У меня до сих пор нет функтора.

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

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

Может ли карта принимать функцию с более чем одним параметром?

Если какая-то функция принимает более одного параметра и вы map используете ее для какого-либо функтора, она будет частично применена и застрянет в эффекте.

Возьмем, к примеру, списки.

add(left)(right) = left + right
map(add)([1,2,3])
-- [ add(1),
--   add(2),
--   add(3) ]

add был частично применен к 1, 2 и 3 для трех частичных приложений add, застрявших в списке. Частично применено, что означает, что функция ожидает других своих параметров. add(1) например ждет еще один номер.

[ add(1),
  add(2),
  add(3) ]

⚠️ На этом вы застряли. Вы не можете передать этот результат обратно в map, чтобы предоставить дополнительные данные для adds. map не принимает частично примененные функции - ожидающие внутри эффекта / списка - поэтому для работы с этим результатом вам понадобится аппликативный функтор.

Что такое аппликативные функторы?

Аппликативный функтор - это список.

pure(input) = [input]
apply(functions)(list) =
  [ element | function <- functions,
              element  <- map(function)(list)
  ]
apply(pure(add1))([1,2,3])
-- [2,3,4]
apply(apply(pure(add))([1,2,3]))([4,5,6])
-- [5,6,7,6,7,8,7,8,9]
apply(map(add)([1,2,3]))([4,5,6])        
-- [5,6,7,6,7,8,7,8,9]

Он возвращает [5, 6, 7, 6, 7, 8, 7, 8, 9], потому что добавляет 1 к 4, 5 и 6, добавляет 2 к 4, 5 и 6, а затем добавляет 3 к 4, 5 и 6.

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

Аппликативный функтор списка вычисляет все возможные комбинации или варианты при упорядочивании или составлении списков. Вы можете представить это в виде дерева.

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

Вызов pure и первый apply заключили три частично примененных add в список, но вы можете заменить pure и один вызов apply на map, поскольку использование add с map также переносит три частично примененных add в список. Мы видели это выше с помощью функтора.

apply(pure(add))([1,2,3])
-- [ add(1),
--   add(2),
--   add(3) ]
map(add)([1,2,3])
-- [ add(1),
--   add(2),
--   add(3) ]

🤔 До сих пор во всех примерах были непустые списки, но что произойдет, если какой-либо один список окажется пустым?

[   ] `apply` [1,2,3] `apply` [4,5,6]
-- []
[add] `apply` [     ] `apply` [4,5,6]
-- []
[add] `apply` [1,2,3] `apply` [     ]
-- []

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

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

💡Одним из важных аспектов аппликативного функтора является то, что эффекты разыгрываются до того, как улучшенная / обновленная функция сможет сделать свое дело. Аппликативный функтор похож на Ronco - установи и забудь - покажи гриль 🍗 шаблонов функционального программирования. После того, как мы запустили процесс, нам остается только ждать конечного результата.

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

Допустим, у нас есть функция с именем if_x_then_y_else_z (пример, адаптированный из Аппликативного программирования с эффектами, Конора Макбрайда и Росс Патерсон. В Journal of Functional Programming 18: 1 (2008), страницы 1–13).

if_a_then_b_else_c(a)(b)(c) = if a then b else c
pure(if_a_then_b_else_c) `apply` x `apply` y `apply` z

Он принимает три эффекта и, в зависимости от результата x, выбирает результат из y или z. Поскольку сейчас мы говорим о списках, давайте воспользуемся аппликативным функтором списка.

if_x_then_y_else_z([True])(["y result"])(["z result"])
-- ["y result"]
if_x_then_y_else_z([False])(["y result"])(["z result"])
-- ["z result"]

Итак, если элемент x равен True, мы выбираем элемент y, иначе мы используем z.

if_x_then_y_else_z([True])(["y result"])(["z result"])
-- ["y result"]

Для входов

  • [True]
  • ["y result"]
  • и ["z result"]

неважно, что z держит, оно никогда не будет использоваться. Но обратите внимание, что происходит, когда z пусто.

if_x_then_y_else_z([True])(["y result"])([])
-- []

Вы получаете пустой список, даже если z список не нужен, поскольку x содержит единственный True. Но, как я уже писал выше, если какой-либо из списков пуст, результат тоже будет пустым.

Вы можете попытаться обойти это, подняв / обновив функцию, возвращающую список.

if_x_then_y_else_z(x)(y)(z) =
  apply(pure(\ a -> if a then y else z))(x)
if_x_then_y_else_z([True])(["y result"])([])                           
-- [["y result"]]

⚠️ Но теперь ты застрял. У вас есть вложенные эффекты или, в данном случае, вложенные списки. Позже вы увидите, как монада списка справляется с этой ситуацией.

💡При использовании аппликативного функтора важно помнить, что повышенная / обновленная функция не может изменять / влиять на форму эффекта в любое время. Для list форма будет размером. Для maybe форма будет just или nothing. Для either форма будет left или right.

Без запуска аппликативного функтора вы можете определить результирующую форму, просто посмотрев на форму заданных эффектов. Учитывая пустой список? Вы получите пустой список. Учитывая [1,2] и [3,4,5]? Вы получите список из шести пунктов.

С монадами все иначе. Они могут изменить форму, потому что могут посмотреть на промежуточные результаты и решить, что делать дальше.

Аппликативный функтор - это функция.

pure(function)(ignore) = function
pure(add1)(Nothing)(0)
-- 1
apply(wrapped_function)(function)(input) =
  map(wrapped_function(input))(function)(input)
apply(pure(add1))(add1)(1)
-- 3
(pure(add) `apply` add1 `apply` add1)(1)
-- 4

pure определение функций - это просто постоянная (const) функция. Постоянная функция принимает два параметра и возвращает первый, игнорируя второй. Таким образом, вызов pure(add1)(Nothing) обертывает, а затем разворачивает add1. Вы можете развернуть любой ввод, который хотите, поскольку константа просто проигнорирует его.

pure(add1)("Ignore this?")(1)
-- 2
pure(add1)(concat ["Ignore"," this?"])(1)
-- 2

Обратите внимание, как я привязал столько вызовов к apply, сколько параметров принимает функция. Я использовал один apply для add1 и два вызова apply для add.

add_x_y_z(x)(y)(z) = add(add(x)(y))(z)
add_x_y_z(2)(2)(2)
-- 6
(pure(add_x_y_z)
`apply` add1
`apply` add1
`apply` add1)(1)
-- 6

Три вызова apply - один для x, один для y и один для z - возвращают 6, потому что

  • 1 добавляется к 1, что делает x равным 2
  • 1 добавляется к 1, что делает y равным 2
  • 1 добавляется к 1, что делает z равным 2
  • и, наконец, x, y и z складываются вместе, и в итоге получается 6.
apply(pure(add_x_y_z))(add1)(1)(2)(3)
-- 7
add_x_y_z(add1)(2)(3)
-- 7

Вам не нужно связывать вызов apply для каждого аргумента. В этом примере я изменил параметр x на add1, но передал 2 как y и 3 как z непосредственно в add_x_y_z.

add1(input) = 1 + input
sub2(input) = 2 - input                                 
sub3(input) = 3 - input
                                 
(pure(add_x_y_z) `apply` add1 `apply` sub2 `apply` sub3)(4)
-- 2
add1(4) + sub2(4) + sub3(4)
-- 2
(1 + 4) + (2 - 4) + (3 - 4)
-- 2

Если вы измените каждый параметр с помощью apply, результирующая функция будет принимать только один ввод, который перейдет к каждому измененному параметру. В этом примере 2 перешел к каждому измененному параметру add_x_y_z.

first  = \ (a,b,c) -> a                                                               
second = \ (a,b,c) -> b                                                              
third  = \ (a,b,c) -> c 
new_add_x_y_z =
  pure(add_x_y_z) `apply` first `apply` second `apply` third         
new_add_x_y_z(1,2,3)
-- 6

Если форма ваших входных данных не соответствует сигнатуре параметра вашей функции, вам пригодится использование apply для компоновки вашей функции с некоторыми другими функциями. В этом примере данные были в кортеже, поэтому я составил add_x_y_z с first, second и third для извлечения, а затем сложил вместе 1, 2 и 3.

extract_then_add_x_y_z(t) =
  add_x_y_z(first(t))(second(t))(third(t))               
extract_then_add_x_y_z(1,2,3)
-- 6

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

first  = \ (a,b) -> a
second = \ (a,b) -> b
new_add_x_y_z = pure(add_x_y_z) `apply` first `apply` second
new_add_x_y_z(1,2)(3)                                         
-- 6

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

Аппликативный функтор - это функтор, который вы законно определяете как чистый и на который претендуете.

pure
  :: input         -- Takes some input.
  -> effect(input) -- Returns it inside an effect.
apply
  :: effect(input -> output) -- Takes a function in an effect.
  -> effect(input          ) -- Takes input in an effect.
  -> effect(         output) -- Returns output in an effect.

В общем, аппликативный функтор - это функтор, для которого вы определяете apply и pure. Функция apply

  • принимает функцию внутри эффекта effect(input -> output),
  • принимает входные данные для этой функции внутри эффекта effect(input),
  • применяет функцию к входу,
  • и возвращает результат функции внутри эффекта effect(output).

💡 Сравните это с картой, и вы увидите небольшое изменение - теперь функция тоже действует.

map
  ::       (input -> output) -- Takes a function.
  -> effect(input          ) -- Takes input inside an effect.
  -> effect(         output) -- Returns output inside an effect.
apply
  :: effect(input -> output) -- Takes a function in an effect.
  -> effect(input          ) -- Takes input in an effect.
  -> effect(         output) -- Returns output in an effect.

👀 Посмотрите еще раз, но в этой сжатой форме.

map   ::  (i -> o) -> e(i) -> e(o)
apply :: e(i -> o) -> e(i) -> e(o)
--       ^ The big difference.

Это основная и единственная разница между apply и map.

Имеется в виду, что у аппликативного функтора тоже есть законы?

да. Есть четыре закона аппликативных функторов.

Значит, аппликативный функтор - это тоже интерфейс?

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

У меня до сих пор нет аппликативного функтора.

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

С точки зрения интерфейса, аппликативный функтор добавляет pure и apply к map функтора.

Что вы имеете в виду под независимыми эффектами?

Независимое значение одного эффекта не зависит от результата другого эффекта.

x = [1,2]
y = [3,4]
z = [5,6]
pure(add) `apply` (pure(add) `apply` x `apply` y) `apply` z  
-- [9,10,10,11,10,11,11,12]

В этом примере мы упорядочиваем три эффекта или списка. Ни x, y, ни z не полагаются на результат любого другого. Поскольку все они независимы, мы можем обрабатывать конечный результат параллельно или одновременно. Это одно из преимуществ аппликативного функтора.

[ 1+3+5=09,
  1+3+6=10,
  1+4+5=10,
  1+4+6=11,
  2+3+5=10,
  2+3+6=11,
  2+4+5=11,
  2+4+6=12 ]

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

add1_inside_effect(input) = pure(input + 1)

Он вернет зависимый эффект / список, зависящий от некоторого элемента input из другого списка.

pure(add1_inside_an_effect) `apply` x                    
-- [[2],[3]]
pure(add) `apply` (pure(add1_inside_an_effect) `apply` x)
-- error!

Аппликативный функтор имеет дело только с независимыми эффектами. Если у вас есть зависимый эффект, вам понадобятся монады, как вы увидите дальше.

Что такое монада?

Монада - это список.

join(nested_lists) =
  [element | list <- nested_lists, element <- list]
join([[1], [2, 3], [4, 5, 6]])
-- [1, 2, 3, 4, 5, 6]
join(apply(pure(\ x -> [x + 1, 1]), [1,2,3]))
-- [2, 1, 3, 1, 4, 1]
join(
  apply(
    pure(\ x -> [2, x * 3, x * 4]))(
    join(
      apply(
        pure(\ x -> [x + 1, 1]))(
        [1,2,3]))))
-- [2,6,8,2,3,4,2,9,12,2,3,4,2,12,16,2,3,4]

Большое и единственное дополнение для интерфейса монад - join. join сглаживает или, лучше сказать, снимает эффект от двух вложенных эффектов. Для экземпляра списка join это просто concat.

Объединение имеет дело только с вложенными эффектами того же типа и вложенными на один уровень глубиной. Так, например, [[1], [3,4]], just(just(1)), right(right(2)), но не [just(1), just(left(1))], [1, [2]], [[[1]]] и т. Д.

if_x_then_y_else_z =
  pure(\ a -> if a then y else z) `apply` x
if_x_then_y_else_z([True])(["y result"])([])                           
-- [["y result"]]

Когда мы ранее подняли / обновили функцию, возвращающую список (чтобы избежать пустого z), мы получили вложенный список. Тогда мы застряли, но join позволяет нам продолжать. Теперь вы можете выбрать список y или z на основе элементов в x и нет получить вложенный список.

if_x_then_y_else_z(x)(y)(z) =
  join(pure(\ a -> if a then y else z) `apply` x)
if_x_then_y_else_z(
  [True,False,False,True])(
  ["y result"])(
  ["z result"])
-- [ "y result",  True  so return y.
--  "z result",   False so return z.
--  "z result",   False so return z.
--  "y result" ]  True  so return y.

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

⚠️ Обратите внимание, что «выбрать следующий эффект» не означает, что вы можете в какой-то момент изменить тип возвращаемого эффекта. Например, вы не можете внезапно решить вернуть может быть, если вы работаете в монаде списка. Монада также является функтором, поэтому в конечном итоге эффект должен оставаться неизменным, и только его результаты могут быть изменены.

Монада - это функция.

join(wrapped_function)(input) =
  wrapped_function(input)(input)
join(apply(pure(add))(add1))(1) 
-- 3
add(add(1)(1))(1)
-- 3
add(2)(1)
-- 3
(1 + 1) + 1
-- 3
divide(numerator)(denominator) = numerator / denominator
join(
  apply(
    pure(divide))(
    join(
      apply(
        pure(add))(
        add1))))(2)
-- 2.5
divide(
  add(
    add(
      1)(
      2))(
    2))(
  2)
-- 2.5
divide(
  add(
    3)(
    2))(
  2)
-- 2.5
divide(
  5)(
  2)
-- 2.5
(((1 + 2) + 2) / 2)
-- 2.5

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

После того, как вы закончите составлять функции вместе с использованием join, apply и pure, возвращаемая функция будет принимать только один ввод. Этот единственный ввод запустит цепную реакцию.

  • Первая функция принимает только один параметр.
  • Он получает ввод и возвращает некоторый вывод.
  • Вторая функция принимает два параметра.
  • Он получает выходные данные от функции один, входные данные, и возвращает некоторые выходные данные.
  • Третья функция принимает два параметра.
  • Он получает выходные данные от функции 2, входные данные, и возвращает некоторые выходные данные.
  • Четвертая функция… и так далее, сколько функций вы упорядочили.

Монада - это аппликативный функтор, для которого вы законно определяете соединение.

join
  :: effect(effect(data))
  -> effect(data)

В общем, монада - это просто аппликативный функтор, для которого вы определяете join.

Итак, монады - это просто аппликативные функторы с добавлением соединения в интерфейс?

да.

Сколько законов для монады?

Есть три закона монад.

А как насчет возврата и привязки?

Bind - это просто вспомогательная функция, которая составляет join, apply и pure.

bind(effect)(function) = join(apply(pure(function))(effect))
[1,2,3] `bind` \ x -> [x + 2]
-- [3,4,5]
[1,2,3] `bind` \ x -> [x + 2] `bind` \ y -> [y * 3]       
-- [9,12,15]
[1,2,3] `bind` \ x -> pure(x + 2) `bind` \ y -> [y * 3, 0] 
-- [9,0,12,0,15,0]
(add1 `bind` add `bind` divide)(2)
-- 2.5

Или, если хотите, bind составляет join и map вместе.

bind(effect)(function) = join(map(function)(effect))
[1,2,3] `bind` \ x -> pure(x + 2) `bind` \ y -> [y * 3, 0] 
-- [9,0,12,0,15,0]

return это просто pure.

return = pure

Можете вы мне их подвести?

  • Функтор поднимает или обновляет функцию, позволяя ей работать с одним эффектом, оставляя эффект нетронутым после его выполнения. Это требует законного map определения.
  • Аппликативный функтор строится на основе или обобщает функтор, позволяя вам упорядочивать несколько независимых эффектов. Это требует законного определения pure и apply.
  • Монада строится на или обобщает аппликативный функтор, позволяя вам упорядочивать независимые и / или зависимые эффекты. Это требует законного join определения.
map   ::  (i ->   o ) -> e(i) -> e(o) -- Functor
apply :: e(i ->   o ) -> e(i) -> e(o) -- Applicative
bind  ::  (i -> e(o)) -> e(i) -> e(o) -- Monad
         ^      ^

Если вы хотите узнать, что вы можете делать с функторами, аппликативами и монадами, ознакомьтесь с Movie Monad и Gifcurry - двумя настольными графическими приложениями, созданными с помощью Haskell, чисто функционального языка программирования.

Дополнительную информацию о функторах, аппликативах, монадах и других шаблонах функционального программирования можно найти в Классопедии типов Брента Йорги .