Есть ли причина, почему «это» аннулируется в методе Керфорда «карри»?

17

В книге Дугласа Крокфорда «Javascript: The Good Parts» он предоставляет код для метода curry , который принимает функцию и аргументы и возвращает эту функцию с уже добавленными аргументами (по-видимому, это не действительно что означает «карри» , но является примером " частичное приложение "). Вот код, который я изменил, чтобы он работал без какого-либо другого настраиваемого кода, который он сделал:

Function.prototype.curry = function(){
  var slice = Array.prototype.slice,
      args = slice.apply(arguments),
      that = this;
  return function() {
    // context set to null, which will cause 'this' to refer to the window
    return that.apply(null, args.concat(slice.apply(arguments)));
  };
};

Итак, если у вас есть функция add :

var add = function(num1, num2) {
  return num1 + num2;
};

add(2, 4);          // returns 6

Вы можете создать новую функцию, у которой уже есть один аргумент:

var add1 = add.curry(1);

add1(2);           // returns 3

Это прекрасно работает. Но что я хочу знать, почему он устанавливает this в null ? Не ожидалось ли, что метод curries совпадает с оригиналом, включая тот же this ?

Моя версия карри будет выглядеть так:

Function.prototype.myCurry = function(){
  var slice = [].slice,
      args = slice.apply(arguments),
      that = this;
  return function() {
    // context set to whatever 'this' is when myCurry is called
    return that.apply(this, args.concat(slice.apply(arguments)));
  };
};

Пример

(вот пример jsfiddle)

var calculator = {
  history: [],
  multiply: function(num1, num2){
    this.history = this.history.concat([num1 + " * " + num2]);
    return num1 * num2;
  },
  back: function(){
    return this.history.pop();
  }
};

var myCalc = Object.create(calculator);
myCalc.multiply(2, 3);         // returns 6
myCalc.back();                 // returns "2 * 3"

Если я попытаюсь сделать это, Дуглас Крокфорд:

myCalc.multiplyPi = myCalc.multiply.curry(Math.PI);
myCalc.multiplyPi(1);          // TypeError: Cannot call method 'concat' of undefined

Если я сделаю это по-своему:

myCalc.multiplyPi = myCalc.multiply.myCurry(Math.PI);
myCalc.multiplyPi(1);          // returns 3.141592653589793
myCalc.back();                 // returns "3.141592653589793 * 1"

Однако мне кажется, что если Дуглас Крокфорд сделал это по-своему, у него, вероятно, есть веская причина. Что мне не хватает?

    
задан RustyToms 08.01.2014 в 18:24
источник
  • Я бы сказал, что это частичное приложение, не особенно выглядящее. Для реализации карри, которая сохраняет контекст, взгляните на то, как работает LiveScript, отлично работает. –  elclanrs 08.01.2014 в 18:31
  • Это на самом деле массив истории, который не определен. Ваше исправление является хорошим! Внедрение Crockford не учитывает использование «классов». –  Kerstomaat 08.01.2014 в 18:40
  • developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... объясняет null там –  epascarello 08.01.2014 в 18:40
  • @epascarello, поэтому значение null заменяется глобальным объектом. Я чувствую, что это ухудшает ситуацию, потому что у вас может быть ситуация, когда ошибка не возникает, даже если это не то, что ожидается. –  RustyToms 08.01.2014 в 18:57
  • @elclanrs, мне понадобится некоторое время, чтобы переварить этот код, но похоже, что необязательный аргумент может использоваться для установки контекста, в противном случае это используется, что кажется мне хорошей идеей. Если я правильно понимаю, что я не уверен, что делаю. –  RustyToms 08.01.2014 в 19:07
Показать остальные комментарии

4 ответа

4

Причина 1 - нелегко обеспечить общее решение

Проблема в том, что ваше решение не является общим. Если вызывающий абонент не назначает новую функцию любому объекту или не назначает его совершенно другому объекту, функция multiplyPi перестает работать:

var multiplyPi = myCalc.multiply.myCurry(Math.PI);
multiplyPi(1);  // TypeError: this.history.concat is not a function

Итак, ни Crockford, ни ваше решение не могут гарантировать правильность использования этой функции. Тогда может быть проще сказать, что функция curry работает только на «функции», а не «методы», и установите this в null , чтобы заставить это. Мы можем только предположить, однако, поскольку Крокфорд не упоминает об этом в книге.

Причина 2 - объясняются функции

Если вы спрашиваете «почему Крокфорд не использовал то или это», очень вероятный ответ: «Это не важно в отношении продемонстрированного материала». Крокфорд использует этот пример в раздел Функции . Целью подраздела curry была:

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

Финализация этого для общего использования с объектами не была целью этой главы. Поскольку это проблематично, если даже не невозможно (см. Раздел 1), было бы более учебным, если бы вместо него вместо null было помещено что-то , что могло бы вызвать вопросы, если оно действительно работает или нет (didn ' t помочь в вашем случае, хотя: -)).

Заключение

Тем не менее, я думаю, вы можете быть абсолютно уверены в своем решении! В вашем случае нет особых причин, чтобы следовать решению Crockfords о сбросе this в null . Вы должны знать, что ваше решение работает только при определенных обстоятельствах и не на 100% чист. Тогда чистое «объектно-ориентированное» решение было бы запросить объект создать клон своего метода внутри себя, , чтобы гарантировать, что результирующий метод останется внутри одного и того же объекта.

    
ответ дан TMS 17.01.2014 в 13:38
5

Читатель берегитесь, вы напуганы.

Есть много разговоров о том, когда дело доходит до 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 . Морфизм может иметь только один аргумент. Если вам нужно несколько аргументов, вы можете либо:

  1. Возвращает другой морфизм (т. е. b - другой морфизм). Это карри. Haskell делает это.
  2. Определить 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 , которые не встречаются в функциональных языках:

  1. Параметр this является специализированным. В отличие от других параметров вы не можете просто установить его на произвольный объект. Следовательно, вам нужно использовать call , чтобы указать другое значение для this .
  2. Если вы хотите частично применить функции в JavaScript, вам нужно указать null в качестве первого параметра bind . Аналогично для call и apply .

Объектно-ориентированное программирование не имеет ничего общего с this . Фактически вы также можете написать объектно-ориентированный код в Haskell. Я бы сказал, что Haskell на самом деле является объектно-ориентированным языком программирования, а гораздо лучше, чем Java или C ++.

Урок 3: Языки функционального программирования более объектно ориентированы, чем большинство основных объектно-ориентированных языков программирования. На самом деле объектно-ориентированный код в JavaScript был бы лучше (хотя, по общему признанию, менее читабельным), если он написан в функциональном стиле.

Проблема с объектно-ориентированным кодом в JavaScript - это параметр this . По моему скромному мнению, параметр this не должен обрабатываться иначе, чем формальные параметры (Lua получил это право). Проблема с this заключается в следующем:

  1. Невозможно установить this , как и другие формальные параметры. Вместо этого вы должны использовать call .
  2. Вы должны установить this в null в bind , если хотите только частично применить функцию.

На стороне примечания я только понял, что каждый раздел этой статьи становится длиннее, чем предыдущий раздел. Поэтому я обещаю сохранить следующий (и окончательный) раздел как можно короче.

4. В защиту Дугласа Крокфорда

К настоящему времени вы, должно быть, поняли, что я думаю, что большая часть JavaScript нарушена и что вы должны перейти на Haskell. Мне нравится верить, что Дуглас Крокфорд тоже функциональный программист, и он пытается исправить JavaScript.

Откуда я знаю, что он функциональный программист? Он парень, который:

  1. Пополнил функциональный эквивалент ключевого слова new (a.k.a Object.create ). Если вы этого еще не сделали, вы должны остановить используя ключевое слово new .
  2. Попытка объяснить концепцию монады и 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, объектно-ориентированы, потому что:

  1. В Haskell мы представляем понятия как алгебраические типы данных , которые по существу являются «объектами на стероидах». ADT имеет один или несколько конструкторов, которые могут иметь ноль или более полей данных.
  2. ADT в Haskell имеют связанные функции. Однако, в отличие от основных языков объектно-ориентированного программирования, ADT не обладают функциями. Вместо этого функции специализируются на ADT. На самом деле это хорошо, поскольку ADT открыты для добавления дополнительных методов. На традиционных языках ООП, таких как Java и C ++, они закрыты.
  3. ADT можно создавать экземпляры типов, аналогичные интерфейсам на Java. Следовательно, у вас все еще есть наследование, дисперсия и подтип полиморфизма, но в гораздо менее интрузивной форме. Например, Functor является суперклассом Applicative .

Вышеприведенный код также объектно-ориентирован. Объектом в этом случае является myCalc , который является просто массивом. Он имеет две функции, связанные с ним: multiply и back . Однако он не владеет этими функциями. Как видите, «функциональный» объектно-ориентированный код имеет следующие преимущества:

  1. Объекты не имеют собственных методов. Следовательно, легко связать новые функции с объектами.
  2. Частичное приложение выполняется простым путем каррирования.
  3. Он поддерживает общее программирование.

Поэтому я надеюсь, что это помогло.

    
ответ дан Aadit M Shah 13.01.2014 в 15:55
  • Вам не нужно выполнять функции JS вручную (с помощью функций вложенности), вы можете делать это программно (с помощью функционального метода)! –  Bergi 13.01.2014 в 19:33
  • [] .filter.bind (null, odd) не работает - вы больше не можете называть его на произвольных массивах. Вам нужно будет использовать частное приложение OP [] .filter.myCurry (нечетное)! –  Bergi 13.01.2014 в 19:35
  • Хотя я действительно люблю Haskell и функциональное программирование, я не думаю, что «объектно-ориентированный код в JavaScript был бы лучше, если бы он написан в функциональном стиле». «Лучше» с точки зрения чего? Также утверждение «Языки функционального программирования более объектно-ориентированные, чем большинство основных объектно-ориентированных языков программирования» нуждается в более подробных объяснениях; в то время как я считаю, что могу догадаться, на что вы нацеливаетесь, я не уверен и думаю, что большинство читателей этого не поймут. Может быть, вам лучше превратить этот ответ в одну из ваших интересных сообщений в блоге! –  Bergi 13.01.2014 в 19:39
  • «Отменив это, Крокфорд заставляет вас перестать полагаться на него». Хороший ответ на реальный вопрос! Тем не менее, для повышения от меня вы должны также включить пример кода, как построить калькулятор OPs объектно-ориентированным, функциональным способом без этого. –  Bergi 13.01.2014 в 19:42
  • @AaditMShah, вы, вероятно, прав насчет Crockford, BTW. Причина, по которой я, Дуглас Крокфорд и другие люди используют javascript, в основном потому, что мы работаем над веб-сайтами, и нам нужен код, который будет запускаться в каждом браузере без необходимости устанавливать что-либо. Поэтому мы должны использовать javascript, я не мог бы использовать Haskell, если бы захотел. –  RustyToms 15.01.2014 в 23:24
Показать остальные комментарии
4
  

Но что я хочу знать, почему он устанавливает это значение null?

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

  

Не ожидалось ли, что метод curried совпадает с оригиналом, включая то же самое?

Да, ваша реализация имеет гораздо больший смысл, однако нельзя ожидать, что частично применяемая функция все же должна быть вызвана в правильном контексте (как и при повторном назначении ее вашему объекту), если она использует ее.

Для них вы можете посмотреть bind метод объектов функций для частичного приложения, включая конкретный this -value.

    
ответ дан Bergi 08.01.2014 в 19:54
2

Из MDN :

  

thisArg. Это значение для вызова для развлечения. Обратите внимание, что это   может быть не фактическое значение, рассматриваемое методом: если метод является   функция в коде нестрогого режима, null и undefined будут заменены   с глобальным объектом, и примитивные значения будут помещены в коробку.

Следовательно, если метод находится в нестрогом режиме, а первый аргумент равен null или undefined , this внутри этого метода будет ссылаться на Window . В строгом режиме это null или undefined . Я добавил живой пример в этой скрипте .

Кроме того, передача null или undefined не наносит вреда, если функция вообще не ссылается на this . Вероятно, именно поэтому Крокфорд использовал null в своем примере, чтобы не перегружать вещи.

    
ответ дан thomaux 13.01.2014 в 10:55