Подтвердить что ты не робот

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

В книге Дугласа Крокфорда "Javascript: Good Parts" он предоставляет код для метода curry, который принимает функцию и аргументы и возвращает эту функцию с уже добавленными аргументами (по-видимому, это не действительно то, что означает "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"

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

Заключение

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

Ответ 2

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

Там много говорить о том, когда дело доходит до 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 не указаны. Вам нужно вручную выварить их и что боль. Поэтому мы используем частичное приложение (вместо

main = print $ add(2, 3)

add :: (Int, Int) -> Int
add(a, b) = a + b

Вы можете преобразовать функцию в своей карри-форме в ее неповрежденную форму и наоборот, используя и функции в 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 аналогична функции application (или $) в 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 или С++.

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

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

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

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

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

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

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

Пополнил функциональный эквивалент ключевого слова new (a.k.a Object.create). Если вы этого еще не сделали, вы должны . Попытка объяснить концепцию сообщество JavaScript.

В любом случае, я думаю, что Crockford аннулировал this в функции curry, потому что он знает, насколько плох this. Было бы святотатством установить его на что-либо кроме null в книге под названием "JavaScript: хорошие части". Я думаю, что он делает мир лучшим местом для одной функции за раз.

Уничтожая this, Крокфорд заставляет вас перестать полагаться на него.

Изменить: По просьбе Берги я опишу более функциональный способ написания объектно-ориентированного кода Calculator. Мы будем использовать метод Крокфорда curry. Начнем с функций 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 = [];

Теперь вы можете использовать метод Crockford curry для частичного применения:

var multiplyPi = multiply.curry(Math.PI);

Затем мы создадим функцию test на multiplyPi на единицу и вернемся к предыдущему состоянию:

var test = bindState(multiplyPi.curry(1), function (prod) {
    alert(prod);
    return back;
});

Если вам не нравится синтаксис, вы можете переключиться на :

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:

Итак, как этот объект кода ориентирован? Википедия определяет как:

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

В соответствии с этим определением функциональные языки программирования, такие как Haskell, объектно-ориентированы, потому что:

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

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

Из MDN:

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

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

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