Есть ли контрпример, что Функторы не могут делать то, что могут делать Монады, кроме надежности функциональной композиции путем выравнивания вложенной структуры?
Я думаю, что это важный момент:
Monads - это небольшое расширение для надежности композиции функций путем объединения: M(M(X)) -> M(X)
, чтобы избежать вложенного типа.
«Избегать вложенного типа» на самом деле не цель join
, это просто аккуратный побочный эффект. То, как вы это делаете, звучит так, будто join
просто удаляет внешний тип, но значение монады не изменяется. Это не вариант. join
является сердцем монады, и именно это позволяет монаде делать вещи .
Вы можете думать о функторе как о контейнере. Есть произвольный внутренний тип, и вокруг него внешняя структура, которая позволяет некоторой дисперсии, некоторым дополнительным значениям «украшать» ваше внутреннее значение. fmap
позволяет вам работать с вещами внутри контейнера, так же, как вы обычно работаете с ними. Это в основном предел того, что вы можете сделать с функтором.
Монада - это функтор с особой силой: где fmap
позволяет вам работать с внутренним значением, bind
позволяет вам комбинировать внешние значения согласованным образом. Это гораздо мощнее простого функтора.
Дело в том, что мы можем реализовать все, что захотим, в структуре Functor, и в этом случае я просто сделал это IO / console.log
значением.
Это на самом деле неверно. Единственная причина, по которой вы смогли сделать IO, заключается в том, что вы используете Javascript, и вы можете делать IO где угодно. На чисто функциональном языке, таком как Haskell, ввод-вывод не может быть выполнен в таком функторе, как этот.
Это грубое обобщение, но по большей части полезно описать IO
как прославленную State
монаду. Каждое действие IO
принимает дополнительный скрытый параметр, называемый RealWorld
(который представляет состояние реального мира), может считывать его или изменять его, а затем отправляет его на следующее действие IO
. Этот параметр RealWorld
пронизывает цепочку. Если что-то записывается на экран, это RealWorld
копируется, модифицируется и передается. Но как работает «прохождение»? Ответ join
.
Скажем, мы хотим прочитать строку от пользователя и вывести ее обратно на экран:
getLine :: IO String
putStrLn :: String -> IO ()
main :: IO ()
main = -- ?
Предположим, IO
- функтор. Как мы это реализуем?
main :: IO (IO ())
main = fmap putStrLn getLine
Здесь мы подняли putStrLn
до IO
, чтобы получить fmap putStrLn :: IO String -> IO (IO ())
. Если вы помните, putStrLn
принимает String
и скрытый RealWorld
и возвращает измененный RealWorld
, где параметр String
выводится на экран. Мы подняли эту функцию с fmap
, так что теперь она принимает IO
(это действие, которое принимает скрытое RealWorld
, возвращает измененные RealWorld
и String
) и возвращает то же самое действие io , только что обернутое вокруг другого значения (совершенно отдельное действие, которое также принимает отдельное скрытое RealWorld
и возвращает RealWorld
). Даже после применения getLine
к этой функции на самом деле ничего не происходит и не печатается.
Теперь у нас есть main :: IO (IO ())
. Это действие, которое принимает скрытое значение RealWorld
и возвращает измененное значение RealWorld
и отдельное действие. Это второе действие принимает другой RealWorld
и возвращает другой измененный RealWorld
. Само по себе это бессмысленно, ничего вам не приносит и ничего не выводит на экран. Что должно произойти, так это то, что два IO
действия должны быть соединены вместе, так что возвращаемое одно действие RealWorld
вводится как параметр RealWorld
другого действия. Таким образом, это становится одной непрерывной цепочкой RealWorld
s, которая мутирует с течением времени. Это «соединение» или «сцепление» происходит, когда два действия IO
объединены с join
.
Конечно, join
делает разные вещи в зависимости от того, с какой монадой вы работаете, но для монад типа IO
и State
это более или менее то, что происходит под капотом. Есть множество ситуаций, когда вы делаете что-то очень простое, не требующее join
, и в этих случаях легко рассматривать монаду как функтор или аппликативный функтор. Но обычно этого недостаточно, и в этих случаях мы используем монады.
РЕДАКТИРОВАТЬ: Ответы на комментарии и отредактированный вопрос:
Я не вижу никакого определения Монад в теории категорий, объясняющего это. Все, что я прочитал о соединении, это стиль MMX => MX
, и это именно то, что делает мой код.
Вы также можете точно сказать, что делает функция String -> String
? Может ли он не возвращать входные данные дословно, инвертировать его, фильтровать, добавлять к нему, игнорировать и возвращать совершенно другое значение или что-либо еще, что приводит к String
? Тип не определяет, что функция делает, он ограничивает , что функция может делать. Поскольку join
в общем случае определяется только его типом, любая конкретная монада может делать все, что разрешено этим типом . Это может быть просто удаление внешнего слоя, или это может быть какой-то чрезвычайно сложный метод объединения двух слоев в один. Пока вы начинаете с двух слоев и заканчиваете одним слоем, это не имеет значения. Тип допускает ряд возможностей, которые являются частью того, что делает монады такими мощными для начала.
В Хаскеле есть MaybeFunctor. Там нет «присоединиться» или «связать» там, и мне интересно, откуда приходит сила. В чем разница между MaybeFunctor и MaybeMonad?
Каждая монада также является функтором: монада является не чем иным, как функтором, который также имеет функцию join
. Если вы используете join
или bind
с Maybe
, вы используете его как монаду, и она обладает всей мощью монады. Если вы не используете join
или bind
, но используете только fmap
и pure
, вы используете его как функтор, и он становится ограниченным действиями, которые может делать функтор. Если нет join
или bind
, то нет дополнительной мощи монады.
Я полагаю, что «избегание вложенного типа» - это не просто аккуратный побочный эффект, но определение «соединения» Монады в теории категорий
Определение join
- это преобразование из вложенной монады в не вложенную монаду. Опять же, это может означать что угодно . Сказать, что цель join
- «избежать вложенного типа», все равно, что сказать, что цель +
- избежать пар чисел. Большинство операций каким-то образом объединяют вещи, но очень немногие из этих операций существуют просто ради того, чтобы иметь комбинацию вещей. Важно как происходит объединение .
в Haskell есть Возможно, функтор , у которого нет join
, или есть Свободная монада , которая join
встроена с первого места в определенную структуру. Это объекты, которые пользователи определяют Функторы для делают вещи .
Я уже обсуждал Maybe
, и как, когда вы используете его только как функтор, он не может делать то, что он может делать, если вы используете его как монаду. Free
странно, потому что это одна из немногих монад, которые на самом деле ничего не делают .
Free
можно использовать для превращения любого функтора в монаду, что позволяет использовать нотацию do
и другие удобства. Однако самонадеянность Free
состоит в том, что join
не объединяет ваши действия так, как это делают другие монады, вместо этого он сохраняет их отдельно, вставляя их в структуру, похожую на список; идея состоит в том, что эта структура позднее обрабатывается, а действия объединяются отдельным кодом 1170 *. Эквивалентный подход состоял бы в том, чтобы переместить этот код обработки в саму join
, но это превратило бы функтор в монаду, и не было бы никакого смысла в использовании Free
. Таким образом, единственная причина, по которой Free
работает, заключается в том, что она делегирует действительную часть "делания" части монады в другом месте; его join
предпочитает отложить действие для кода, работающего вне монады. Это похоже на оператор +
, который вместо добавления чисел возвращает абстрактное синтаксическое дерево; затем можно было обработать это дерево позже любым необходимым способом.
Эти наблюдения не соответствуют факту существования функтора Maybe и Свободной монады.
Вы не правы. Как объяснено, Maybe
и Free
отлично вписываются в мои предыдущие наблюдения:
- Функтор
Maybe
просто не обладает той же выразительностью, что и монада Maybe
.
- Монада
Free
преобразует функторы в монады единственным возможным способом: не реализуя монадическое поведение, а вместо этого просто откладывая его до некоторого предполагаемого кода обработки.