Читатель берегитесь, вы напуганы.
Есть много разговоров о том, когда дело доходит до currying, функций, частичного приложения и объектной ориентации в JavaScript. Я постараюсь, чтобы этот ответ был как можно короче, но есть что обсудить. Поэтому я структурировал свою статью по нескольким разделам, и в конце каждого из них я обобщил каждый раздел для тех из вас, кто слишком нетерпелив, чтобы читать все это.
1. Карри или не карри
Давайте поговорим о Хаскелле. В Haskell каждая функция имеет значение по умолчанию. Например, мы могли бы создать функцию add
в Haskell следующим образом:
add :: Int -> Int -> Int
add a b = a + b
Обратите внимание на подпись типа Int -> Int -> Int
? Это означает, что add
принимает Int
и возвращает функцию типа Int -> Int
, которая в свою очередь принимает Int
и возвращает Int
. Это позволяет вам частично применять функции в Haskell легко:
add2 :: Int -> Int
add2 = add 2
Такая же функция в JavaScript выглядела бы уродливо:
function add(a) {
return function (b) {
return a + b;
};
}
var add2 = add(2);
Проблема в том, что по умолчанию функции JavaScript не указаны. Вам нужно вручную выварить их, и это боль. Следовательно, мы используем частичное приложение (aka uncurry
и curry
в Haskell соответственно. Необработанная функция в Haskell по-прежнему принимает только один аргумент. Однако этот аргумент является произведением нескольких значений (например, тип продукта ). р>
В том же вене функции в JavaScript также принимают только один аргумент (он еще этого еще не знает). Этот аргумент является типом продукта. Значение arguments
внутри функции является проявлением этого типа продукта. Это иллюстрируется методом apply
в JavaScript, который берет тип продукта и применяет к нему функцию. Например:
print(add.apply(null, [2, 3]));
Вы видите сходство между приведенной выше строкой в JavaScript и следующей строкой в Haskell?
main = print $ add(2, 3)
Игнорируйте присвоение main
, если вы не знаете, для чего это необходимо. Это не имеет отношения к теме. Важно то, что кортеж (2, 3)
в Haskell изоморфен массиву [2, 3]
в JavaScript. Что мы узнаем из этого?
Функция apply
в JavaScript такая же, как приложение-приложение (или $
) в Haskell:
($) :: (a -> b) -> a -> b
f $ a = f a
Возьмем функцию типа a -> b
и применим ее к значению типа a
, чтобы получить значение типа b
. Однако, поскольку все функции в JavaScript не выполняются по умолчанию, функция apply
всегда принимает тип продукта (т. Е. Массив) в качестве второго аргумента. То есть, значение типа a
фактически является типом продукта в JavaScript.
Урок 2: Все функции JavaScript используют только один аргумент, который является типом продукта (т. е. значение arguments
). Было ли это предположение или случайность - вопрос спекуляции. Однако важно то, что вы понимаете, что математически каждая функция принимает только один аргумент.
Математически функция определяется как морфизм : a -> b
, Он принимает значение типа a
и возвращает значение типа b
. Морфизм может иметь только один аргумент. Если вам нужно несколько аргументов, вы можете либо:
- Возвращает другой морфизм (т. е.
b
- другой морфизм). Это карри. Haskell делает это.
- Определить
a
как произведение нескольких типов (т. е. a
- тип продукта). JavaScript делает это.
Из двух я предпочитаю карри-функции, поскольку они делают частичное приложение тривиальным. Частичное применение «неуправляемых» функций сложнее. Не сложно, заметьте, но еще сложнее. Это одна из причин, по которой мне нравится Haskell больше, чем JavaScript: по умолчанию выполняются функции.
3. Почему ООП не имеет значения
Давайте посмотрим на некоторый объектно-ориентированный код в JavaScript. Например:
var oddities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filter(odd).length;
function odd(n) {
return n % 2 !== 0;
}
Теперь вы можете задаться вопросом, как это объектно-ориентированное.Это больше похоже на функциональный код. В конце концов вы можете сделать то же самое в Haskell:
oddities = length . filter odd $ [0..9]
Тем не менее приведенный выше код является объектно-ориентированным. Литерал массива - это объект, который имеет метод filter
, который возвращает новый объект массива. Затем мы просто получаем доступ к length
нового объекта массива.
Что мы узнаем из этого? Цепочки операций в объектно-ориентированных языках такие же, как и составные функции в функциональных языках. Единственное отличие состоит в том, что функциональный код читается в обратном порядке. Давайте посмотрим, почему.
В JavaScript параметр this
является особенным. Он отличается от формальных параметров функции, поэтому вам нужно указать значение для нее отдельно в методе apply
. Поскольку this
встречается до формальных параметров, методы привязаны слева направо.
add.apply(null, [2, 3]); // this comes before the formal parameters
Если this
должно было появиться после формальных параметров, указанный выше код, вероятно, будет читаться как:
var oddities = length.filter(odd).[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
apply([2, 3], null).add; // this comes after the formal parameters
Не очень приятно? Тогда почему функции в Haskell читают назад? Ответ важен. Вы видите, что функции в Haskell также имеют параметр « this
». Однако, в отличие от JavaScript, параметр this
в Haskell не является особенным. Кроме того, он приходит в конце списка аргументов. Например:
filter :: (a -> Bool) -> [a] -> [a]
Функция filter
принимает предикатную функцию и список this
и возвращает новый список только с фильтрованными элементами. Итак, почему последний параметр this
? Это упрощает частичное применение. Например:
filterOdd = filter odd
oddities = length . filterOdd $ [0..9]
В JavaScript вы должны написать:
Array.prototype.filterOdd = [].filter.myCurry(odd);
var oddities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filterOdd().length;
Теперь, какой из них вы бы выбрали? Если вы все еще жалуетесь на чтение назад, у меня есть новости для вас. Вы можете сделать код Haskell прочитанным вперед, используя «обратное приложение» и «обратную композицию» следующим образом:
($>) :: a -> (a -> b) -> b
a $> f = f a
(>>>) :: (a -> b) -> (b -> c) -> (a -> c)
f >>> g = g . f
oddities = [0..9] $> filter odd >>> length
Теперь у вас есть лучшее из обоих миров. Ваш код читается вперед, и вы получаете все преимущества каррирования.
Существует много проблем с this
, которые не встречаются в функциональных языках:
- Параметр
this
является специализированным. В отличие от других параметров вы не можете просто установить его на произвольный объект. Следовательно, вам нужно использовать call
, чтобы указать другое значение для this
.
- Если вы хотите частично применить функции в JavaScript, вам нужно указать
null
в качестве первого параметра bind
. Аналогично для call
и apply
.
Объектно-ориентированное программирование не имеет ничего общего с this
. Фактически вы также можете написать объектно-ориентированный код в Haskell. Я бы сказал, что Haskell на самом деле является объектно-ориентированным языком программирования, а гораздо лучше, чем Java или C ++.
Урок 3: Языки функционального программирования более объектно ориентированы, чем большинство основных объектно-ориентированных языков программирования. На самом деле объектно-ориентированный код в JavaScript был бы лучше (хотя, по общему признанию, менее читабельным), если он написан в функциональном стиле.
Проблема с объектно-ориентированным кодом в JavaScript - это параметр this
. По моему скромному мнению, параметр this
не должен обрабатываться иначе, чем формальные параметры (Lua получил это право). Проблема с this
заключается в следующем:
- Невозможно установить
this
, как и другие формальные параметры. Вместо этого вы должны использовать call
.
- Вы должны установить
this
в null
в bind
, если хотите только частично применить функцию.
На стороне примечания я только понял, что каждый раздел этой статьи становится длиннее, чем предыдущий раздел. Поэтому я обещаю сохранить следующий (и окончательный) раздел как можно короче.
4. В защиту Дугласа Крокфорда
К настоящему времени вы, должно быть, поняли, что я думаю, что большая часть JavaScript нарушена и что вы должны перейти на Haskell. Мне нравится верить, что Дуглас Крокфорд тоже функциональный программист, и он пытается исправить JavaScript.
Откуда я знаю, что он функциональный программист? Он парень, который:
- Пополнил функциональный эквивалент ключевого слова
new
(a.k.a Object.create
). Если вы этого еще не сделали, вы должны остановить используя ключевое слово new
.
- Попытка объяснить концепцию монады и gonads в сообщество JavaScript.
В любом случае, я думаю, что Crockford аннулировал this
в функции curry
, потому что он знает, насколько плохим this
. Было бы кощунством установить его на что-либо иное, кроме null
, в книге под названием «JavaScript: хорошие детали». Я думаю, что он делает мир лучшим местом для одной функции за раз.
Уничтожая this
, Крокфорд заставляет вас перестать полагаться на него.
Изменить: По просьбе Берги я опишу более функциональный способ написать объектно-ориентированный код Calculator
.Мы будем использовать метод curry
от Crockford. Начнем с функций multiply
и back
:
function multiply(a, b, history) {
return [a * b, [a + " * " + b].concat(history)];
}
function back(history) {
return [history[0], history.slice(1)];
}
Как вы видите, функции multiply
и back
не принадлежат ни одному объекту. Следовательно, вы можете использовать их в любом массиве. В частности, ваш класс Calculator
- это всего лишь оболочка для списка строк. Следовательно, вам даже не нужно создавать для него другой тип данных. Следовательно:
var myCalc = [];
Теперь вы можете использовать метод curry
от Crockford для частичного применения:
var multiplyPi = multiply.curry(Math.PI);
Затем мы создадим функцию test
для multiplyPi
на единицу и вернемся к предыдущему состоянию:
var test = bindState(multiplyPi.curry(1), function (prod) {
alert(prod);
return back;
});
Если вам не нравится синтаксис, вы можете перейти на LiveScript : р>
test = do
prod <- bindState multiplyPi.curry 1
alert prod
back
Функция bindState
- это функция bind
государственной монады. Он определяется следующим образом:
function bindState(g, f) {
return function (s) {
var a = g(s);
return f(a[0])(a[1]);
};
}
Итак, давайте вернемся к тесту:
alert(test(myCalc)[0]);
Смотрите демонстрацию здесь: Ссылка
Кстати, вся эта программа была бы более кратким, если бы была написана в LiveScript следующим образом:
multiply = (a, b, history) --> [a * b, [a + " * " + b] ++ history]
back = ([top, ...history]) -> [top, history]
myCalc = []
multiplyPi = multiply Math.PI
bindState = (g, f, s) -->
[a, t] = g s
(f a) t
test = do
prod <- bindState multiplyPi 1
alert prod
back
alert (test myCalc .0)
См. демонстрацию скомпилированного кода LiveScript: Ссылка
Итак, как этот объект кода ориентирован? Wikipedia определяет объектно-ориентированное программирование как: р>
Объектно-ориентированное программирование (ООП) - это парадигма программирования, представляющая понятия как «объекты», которые имеют поля данных (атрибуты, описывающие объект) и связанные с ними процедуры, известные как методы. Объекты, которые обычно являются экземплярами классов, используются для взаимодействия друг с другом при проектировании приложений и компьютерных программ.
В соответствии с этим определением функциональные языки программирования, такие как Haskell, объектно-ориентированы, потому что:
- В Haskell мы представляем понятия как алгебраические типы данных , которые по существу являются «объектами на стероидах». ADT имеет один или несколько конструкторов, которые могут иметь ноль или более полей данных.
- ADT в Haskell имеют связанные функции. Однако, в отличие от основных языков объектно-ориентированного программирования, ADT не обладают функциями. Вместо этого функции специализируются на ADT. На самом деле это хорошо, поскольку ADT открыты для добавления дополнительных методов. На традиционных языках ООП, таких как Java и C ++, они закрыты.
- ADT можно создавать экземпляры типов, аналогичные интерфейсам на Java. Следовательно, у вас все еще есть наследование, дисперсия и подтип полиморфизма, но в гораздо менее интрузивной форме. Например,
Functor
является суперклассом Applicative
.
Вышеприведенный код также объектно-ориентирован. Объектом в этом случае является myCalc
, который является просто массивом. Он имеет две функции, связанные с ним: multiply
и back
. Однако он не владеет этими функциями. Как видите, «функциональный» объектно-ориентированный код имеет следующие преимущества:
- Объекты не имеют собственных методов. Следовательно, легко связать новые функции с объектами.
- Частичное приложение выполняется простым путем каррирования.
- Он поддерживает общее программирование.
Поэтому я надеюсь, что это помогло.