Ответ 1
Прежде всего, наград в библиотеке функционального программирования, которую вы поддерживаете. Я всегда хотел написать его сам, но я не нашел времени для этого.
Учитывая тот факт, что вы пишете библиотеку функционального программирования, я собираюсь предположить, что вы знаете о Haskell. В Haskell у нас есть функции и операторы. Функции всегда являются префиксами. Операторы всегда инфикс.
Функции в Haskell могут быть преобразованы в операторы с использованием обратных ссылок. Например, div 6 3
может быть записано как 6 `div` 3
. Аналогичным образом операторы могут быть преобразованы в функции с помощью круглых скобок. Например, 2 < 3
может быть записано как (<) 2 3
.
Операторы также могут быть частично применены с использованием разделов. Существует два типа разделов: левые секции (например, (2 <)
и (6 `div`)
) и правые секции (например, (< 3)
и (`div` 3)
). Левые разделы переводятся следующим образом: (2 <)
становится (<) 2
. Правые секции: (< 3)
становится flip (<) 3
.
В JavaScript у нас есть только функции. Нет никакого "хорошего" способа создания операторов в JavaScript. Вы можете написать код типа (2).lt(3)
, но, по моему скромному мнению, он нечеткий, и я бы настоятельно советовал не писать такой код.
Таким образом, тривиально мы можем записать нормальные функции и операторы как функции:
div(6, 3) // normal function: div 6 3
lt(2, 3) // operator as a function: (<) 2 3
Написание и внедрение инфиксных операторов в JavaScript - это боль. Следовательно, у нас не будет следующего:
(6).div(3) // function as an operator: 6 `div` 3
(2).lt(3) // normal operator: 2 < 3
Однако разделы важны. Начните с правой части:
div(3) // right section: (`div` 3)
lt(3) // right section: (< 3)
Когда я увижу div(3)
, я ожидаю, что это будет правый раздел (т.е. он должен вести себя как (`div` 3)
). Следовательно, согласно принципу наименьшего удивления, так оно и должно быть реализовано.
Теперь возникает вопрос о левых разделах. Если div(3)
- это правая секция, то как выглядит левая часть? По моему скромному мнению, это должно выглядеть так:
div(6, _) // left section: (6 `div`)
lt(2, _) // left section: (2 <)
Мне это читается как "делить 6 на что-то" и "на 2 меньше чем что-то"? Я предпочитаю этот путь, потому что он явный. Согласно Zen of Python, "Явный лучше, чем неявный."
Итак, как это влияет на существующий код? Например, рассмотрим функцию filter
. Чтобы отфильтровать нечетные числа в списке, мы будем писать filter(odd, list)
. Для такой функции карри работает так, как ожидалось? Например, как мы будем писать функцию filterOdd
?
var filterOdd = filter(odd); // expected solution
var filterOdd = filter(odd, _); // left section, astonished?
В соответствии с принципом наименьшего удивления он должен быть просто filter(odd)
. Функция filter
не предназначена для использования в качестве оператора. Следовательно, программисту не следует принуждать его использовать как левую часть. Должно быть четкое различие между функциями и "функциональными операторами".
К счастью, различение функций и операторов функций довольно интуитивно. Например, функция filter
явно не является оператором функции:
filter odd list -- filter the odd numbers from the list; makes sense
odd `filter` list -- odd filter of list? huh?
С другой стороны, функция elem
, очевидно, является функциональным оператором:
list `elem` n -- element n of the list; makes sense
elem list n -- element list, n? huh?
Важно отметить, что это различие возможно только потому, что функции и операторы функций являются взаимоисключающими. Разумеется, данная функция может быть либо нормальной, либо функциональной, но не той и другой.
Интересно отметить, что при двоичной функции, если вы flip
ее аргументы, она становится двоичным оператором и наоборот. Например, рассмотрите перевернутые варианты filter
и elem
:
list `filter` odd -- now filter makes sense an an operator
elem n list -- now elem makes sense as a function
На самом деле это можно было бы обобщить для любой функции n-arity, если n больше 1. Вы видите, что каждая функция имеет первичный аргумент. Тривиально, что для унарных функций это различие не имеет значения. Однако для не унарных функций это различие важно.
- Если основной аргумент функции находится в конце списка аргументов, функция является нормальной функцией (например,
filter odd list
, гдеlist
является основным аргументом). Для составления функции необходим первичный аргумент в конце списка. - Если основной аргумент функции находится в начале списка аргументов, то функция является функциональным оператором (например,
list `elem` n
, гдеlist
является основным аргументом). - Операторы аналогичны методам в ООП, а первичный аргумент аналогичен объекту метода. Например,
list `elem` n
будет записываться какlist.elem(n)
в ООП. Цепочные методы в ООП аналогичны целям функционального состава в FP [1]. - Основной аргумент функции может быть либо в начале, либо в конце списка аргументов. Не было бы смысла, чтобы это было где-то еще. Это свойство нечетно верно для двоичных функций. Следовательно, переворачивание двоичных функций делает их операторами и наоборот.
- Остальные аргументы вместе с функцией образуют неделимый атом, называемый основой списка аргументов. Например, в
filter odd list
стерженьfilter odd
. Вlist `elem` n
стержень(`elem` n)
. - Порядок и элементы стебля должны оставаться неизменными, чтобы выражение имело смысл. Вот почему
odd `filter` list
иelem list n
не имеют никакого смысла. Однакоlist `filter` odd
иelem n list
имеют смысл, потому что стержень не изменяется.
Возвращаясь к основной теме, поскольку функции и операторы функций являются взаимоисключающими, вы можете просто рассматривать функциональные операторы иначе, чем то, как вы относитесь к нормальным функциям.
Мы хотим, чтобы операторы имели следующее поведение:
div(6, 3) // normal operator: 6 `div` 3
div(6, _) // left section: (6 `div`)
div(3) // right section: (`div` 3)
Мы хотим определить операторы следующим образом:
var div = op(function (a, b) {
return a / b;
});
Определение функции op
прост:
function op(f) {
var length = f.length, _; // we want underscore to be undefined
if (length < 2) throw new Error("Expected binary function.");
var left = R.curry(f), right = R.curry(R.flip(f));
return function (a, b) {
switch (arguments.length) {
case 0: throw new Error("No arguments.");
case 1: return right(a);
case 2: if (b === _) return left(a);
default: return left.apply(null, arguments);
}
};
}
Функция op
аналогична использованию обратных циклов для преобразования функции в оператор в Haskell. Следовательно, вы можете добавить его как стандартную библиотечную функцию для Ramda. Также упоминайте в документах, что основным аргументом оператора должен быть первый аргумент (т.е. Он должен выглядеть как ООП, а не FP).
[1] На стороне примечания было бы замечательно, если бы Ramda разрешил вам создавать функции, как если бы они были целыми методами в обычном JavaScript (например, foo(a, b).bar(c)
вместо compose(bar(c), foo(a, b))
). Это сложно, но выполнимо.