Я думаю, что ваше путаница возникает из-за слишком простых функций. В частности, вы пишете
const inc = x => x + 1;
тип которого является функцией, которая возвращает значения в же пространство как его вход. Скажем, inc
имеет дело с целыми числами. Поскольку и его вход и выход являются целыми числами, если у вас есть другая функция foo
, которая принимает целые числа, легко представить, используя выход из inc
как вход - foo
.
Реальный мир включает в себя более интересные функции. Рассмотрим функцию tree_of_depth
, которая берет целое число и создает дерево строк этой глубины. (Я не буду пытаться реализовать его, потому что я не знаю достаточно javascript, чтобы сделать убедительную работу.) Теперь неожиданно трудно представить, как передать вывод tree_of_depth
в качестве входа в foo
, поскольку foo
является ожидая целых чисел, и tree_of_depth
производит деревья, не так ли? Единственное, что мы можем передать foo
, - это вход - tree_of_depth
, потому что это единственное целое число, которое мы имеем, даже после запуска tree_of_depth
.
Давайте посмотрим, как это проявляется в типе подписи Haskell для привязки:
(>>=) :: (r -> a) -> (a -> r -> b) -> (r -> b)
Это говорит о том, что (>>=)
принимает два аргумента, каждая из функций. Первая функция может быть любого старого типа, который вам нравится - он может принимать значение типа r
и производить значение типа a
. В частности, вам не обязательно обещать, что r
и a
такие же.Но как только вы выбираете его тип, тогда тип следующего аргумента функции для (>>=)
ограничен: он должен быть функцией двух аргументов, типы которых равны тем же и a
, как и раньше.
Теперь вы можете понять, почему мы должны передать такое же значение типа r
на обе эти функции: первая функция производит a
, не обновленный r
, поэтому мы не имеем никакого другого значения типа r
перейти к вторая функция! В отличие от вашей ситуации с inc
, где первая функция произошла также с , произведитеr
, мы можем производить некоторые другие очень разные типы.
Это объясняет, почему привязка должна быть реализована так, как она есть, но, возможно, не объясняет, почему эта монада является полезной. Там написано в другом месте. Но канонический вариант использования - для переменных конфигурации. Предположим, что при запуске программы вы разбираете файл конфигурации; то для остальной части программы вы хотите иметь возможность влиять на поведение различных функций, просматривая информацию из этой конфигурации. Во всех случаях имеет смысл использовать одну и ту же конфигурационную информацию - ее не нужно менять. Затем эта монада становится полезной: вы можете иметь неявное значение конфигурации, а операция привязки монады гарантирует, что обе функции, которые вы упорядочиваете, имеют доступ к этой информации, не передавая ее вручную обеим функциям.
P.S. Вы говорите
Тем более удивительно, что монадическая функция не дает результат предыдущего вычисления следующему.
который я нахожу немного неточным: ведь в m >>= f
, функция f
получает как результатом m
(в качестве первого аргумента) и исходное значение (в качестве второго аргумента).
Правило большого пальца: 'bind (...) (x => ...)' принимает значение предыдущего вычисления и связывает его с 'x'. Другие lambdas 'x => ...' (а не после привязки) вместо этого получают доступ к одному и тому же неявному аргументу только для чтения, который сначала передается в 'f'. – chi
Сторона примечания: 'x => x <= 5 ? x => x * 2: x => x * 3' нечитабельно, пожалуйста, используйте, например. 'x => x <= 5 ? y => y * 2: z => z * 3' - компьютер не заботится об альфа-эквивалентности, но люди делают ;-) – chi
Потому что он * должен * - эта функция является единственной действительной реализацией функция, тип которой 'forall rab. (r -> a) -> (a -> (r -> b)) -> r -> b' (кроме неопределенного, конечно). Функция javascript может, конечно, делать то, что ему нравится, в отсутствие формальных типов, но тогда рассуждение об этом было бы намного сложнее (и назвал его «bind» было бы просто ошибкой). – user2407038