18

在 Douglas Crockford 的“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?预期的行为不会是咖喱方法与原始方法相同,包括相同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"

如果我尝试按照 Douglas Crockford 的方式进行操作:

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"

但是,我觉得如果道格拉斯·克罗克福德按照自己的方式行事,他可能有充分的理由。我错过了什么?

4

4 回答 4

5

读者当心,你被吓到了。

当谈到 JavaScript 中的柯里化、函数、部分应用程序和面向对象时,有很多东西要讨论。我会尽量让这个答案尽可能简短,但有很多要讨论的。因此,我将我的文章分为几个部分,并在每个部分的末尾为那些迫不及待地阅读所有内容的人总结了每个部分。


1. 咖喱还是不咖喱

让我们谈谈Haskell。在 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 bind)来代替。

第 1 课: Currying 用于更轻松地部分应用函数。然而,它只在函数默认柯里化的语言中有效(例如 Haskell)。如果您必须手动 curry 函数,那么最好使用部分应用程序。


2.函数的结构

Haskell 中也存在非curried 函数。它们看起来像“普通”编程语言中的函数:

main = print $ add(2, 3)

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

您可以分别使用 Haskell中的uncurry和函数将其柯里化形式的函数转换为其非柯里化形式,反之亦然。curryHaskell 中的非柯里化函数仍然只接受一个参数。但是,该参数是多个值的乘积(即产品类型)。

同样,JavaScript 中的函数也只接受一个参数(只是还不知道)。该参数是一种产品类型。arguments函数中的值是该产品类型的体现。这以 JavaScript 中的方法为例,该apply方法采用产品类型并将函数应用于它。例如:

print(add.apply(null, [2, 3]));

你能看出 JavaScript 中的上述行和 Haskell 中的以下行之间的相似之处吗?

main = print $ add(2, 3)

main如果您不知道它的用途,请忽略分配给。这与手头的主题无关。重要的是 Haskell 中的元组与 JavaScript(2, 3)中的数组同构。[2, 3]我们从中学到什么?

JavaScript 中的函数与 Haskellapply中的函数应用程序(或)相同:$

($) :: (a -> b) -> a -> b
f $ a = f a

我们采用 type 的函数a -> b并将其应用于 type 的值a以获取 type 的值b。然而,由于 JavaScript 中的所有函数默认情况下都是非柯里化的,因此该apply函数总是将产品类型(即数组)作为其第二个参数。也就是说 type 的值a实际上是 JavaScript 中的一种产品类型。

第 2 课: JavaScript 中的所有函数都只接受一个参数,即产品类型(即arguments值)。这是有意的还是偶然的,这是一个猜测的问题。然而重要的一点是你明白数学上每个函数只接受一个参数。

在数学上,函数被定义为态射: a -> b。它接受一个类型的值a并返回一个类型的值b。态射只能有一个参数。如果您想要多个参数,那么您可以:

  1. 返回另一个态射(即b是另一个态射)。这是咖喱。Haskell 就是这样做的。
  2. 定义a为多种类型的产品(即a是一种产品类型)。JavaScript 就是这样做的。

在这两个中,我更喜欢 curried 函数,因为它们使部分应用程序变得微不足道。“uncurried”函数的部分应用更复杂。请注意,这并不难,只是更复杂。这就是我喜欢 Haskell 而不是 JavaScript 的原因之一:默认情况下,函数是柯里化的。


3. 为什么 OOP 不重要

让我们看一下 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. 同样对于callapply

面向对象编程与this. 事实上,您也可以在 Haskell 中编写面向对象的代码。我什至可以说 Haskell 实际上是一种面向对象的编程语言,并且在这方面比 Java 或 C++ 要好得多。

第 3 课:函数式编程语言比大多数主流的面向对象编程语言更面向对象。事实上,如果以函数式风格编写,JavaScript 中的面向对象代码会更好(尽管可读性较差)。

JavaScript 中面向对象代码的问题在于this参数。在我看来,this参数的处理方式不应与形式参数有任何不同(Lua 是正确的)。问题this在于:

  1. 没有办法this像其他形式参数一样设置。你必须call改用。
  2. 如果您只想部分应用某个功能,则必须设置thisnullin 。bind

顺便说一句,我刚刚意识到这篇文章的每个部分都比上一节要长。因此,我保证下一个(也是最后一个)部分尽可能短。


4. 为道格拉斯·克罗克福德辩护

到目前为止,您一定已经意识到我认为大部分 JavaScript 都已损坏,您应该改用 Haskell。我喜欢相信 Douglas Crockford 也是一个函数式程序员,并且他正在尝试修复 JavaScript。

我怎么知道他是一个函数式程序员?他是这样的人:

  1. 推广new关键字(aka Object.create)的功能等价物。如果您还没有这样做,那么您应该停止使用new关键字
  2. 试图向JavaScript 社区解释monads 和 gonads的概念。

无论如何,我认为 Crockfordthiscurry函数中无效,因为他知道有多糟糕this。将它设置为除了null一本名为“JavaScript:The Good Parts”的书中之外的任何东西都是一种亵渎。我认为他正在让世界变得更美好,一次一个功能。

取消thisCrockford 会迫使您停止依赖它。


编辑:按照 Bergi 的要求,我将描述一种更实用的方式来编写面向对象的Calculator代码。我们将使用 Crockford 的curry方法。让我们从multiplyandback函数开始:

function multiply(a, b, history) {
    return [a * b, [a + " * " + b].concat(history)];
}

function back(history) {
    return [history[0], history.slice(1)];
}

如您所见,multiplyandback函数不属于任何对象。因此,您可以在任何阵列上使用它们。特别是您的Calculator类只是字符串列表的包装器。因此,您甚至不需要为它创建不同的数据类型。因此:

var myCalc = [];

现在您可以使用 Crockford 的curry方法进行部分应用:

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

接下来我们将创建一个test函数 to multiplyPiby one 并返回到之前的状态:

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]);

在此处查看演示:http: //jsfiddle.net/5h5R9/

顺便说一句,如果用 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 代码的演示:http: //jsfiddle.net/5h5R9/1/

那么这段代码是如何面向对象的呢?维基百科将面向对象编程定义为:

面向对象编程 (OOP) 是一种编程范式,它将概念表示为“对象”,这些“对象”具有数据字段(描述对象的属性)和称为方法的相关过程。对象通常是类的实例,用于相互交互以设计应用程序和计算机程序。

根据这个定义,像 Haskell 这样的函数式编程语言是面向对象的,因为:

  1. 在 Haskell 中,我们将概念表示为代数数据类型,它们本质上是“类固醇对象”。ADT 有一个或多个构造函数,这些构造函数可能有零个或多个数据字段。
  2. Haskell 中的 ADT 具有相关的功能。然而,与主流的面向对象编程语言不同,ADT 不拥有这些功能。相反,这些功能专门针对 ADT。这实际上是一件好事,因为 ADT 对添加更多方法持开放态度。在 Java 和 C++ 等传统 OOP 语言中,它们是封闭的。
  3. ADT 可以成为类似于 Java 中的接口的类型类的实例。因此,您仍然有继承、变异和子类型多态性,但侵入性要小得多。例如Functor是 的超类Applicative

上面的代码也是面向对象的。在这种情况下,对象myCalc只是一个数组。它有两个与之相关的功能:multiplyback。但是它不拥有这些功能。如您所见,“功能性”面向对象的代码具有以下优点:

  1. 对象不拥有方法。因此很容易将新功能与对象相关联。
  2. 部分应用通过柯里化变得简单。
  3. 它促进了泛型编程。

所以我希望这会有所帮助。

于 2014-01-13T14:55:10.510 回答
4

但我想知道的是他为什么将它设置为null?

没有真正的原因。可能他想简化,并且大多数对柯里化或部分应用有意义的函数不是使用this. 在更实用的样式中history,附加到的数组将是函数的另一个参数(甚至可能是返回值)。

不预期的行为是咖喱方法与原始方法相同,包括相同的 this 吗?

是的,您的实现更有意义,但是如果它使用一个部分应用的函数,那么人们可能不会期望它仍然需要在正确的上下文中调用(就像您通过将其重新分配给您的对象所做的那样)。

对于那些,您可能会查看用于部分应用的 Function 对象的bind方法,包括特定的this值。

于 2014-01-08T18:54:04.227 回答
4

原因 1 - 不容易提供通用解决方案

问题是您的解决方案不通用。如果调用者没有将新函数分配给任何对象,或者将其分配给完全不同的对象,您的multiplyPi函数将停止工作:

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

因此,Crockford 和您的解决方案都不能确保正确使用该功能。那么可能更容易说该curry函数仅适用于“函数”,而不适用于“方法”,并设置thisnull强制执行。我们可能只是推测,因为克罗克福德在书中没有提到这一点。

原因 2 - 正在解释功能

如果你问“为什么 Crockford 不使用这个或那个” - 很可能的答案是:“这对于已证明的问题并不重要。” Crockford 在函数一章中使用了这个例子。子章节的目的curry是:

  • 表明函数是您可以创建和操作的对象
  • 演示闭包的另一种用法
  • 展示如何操纵论点。

对它进行微调以用于对象的一般用法不是本章的目的。因为即使不是不可能也是有问题的(请参阅原因 1),null如果放在那里可能会引发问题的东西是否真的有效(尽管对您的情况没有帮助:-)) ,所以放在那里更有教育意义.

结论

也就是说,我认为您可以对自己的解决方案充满信心!在您的情况下,没有特别的理由遵循 Crockfords 重新设置thisnull. 您必须知道,您的解决方案仅在某些情况下有效,并且不是 100% 干净的。然后干净的“面向对象”解决方案是要求对象在自身内部创建其方法的克隆,以确保生成的方法将保留在同一个对象中。

于 2014-01-17T12:38:27.823 回答
2

来自MDN

thisArg 为 fun 调用提供的 this 值。请注意,这可能不是方法看到的实际值:如果方法是非严格模式代码中的函数,null 和 undefined 将被替换为全局对象,原始值将被装箱。

因此,如果该方法处于非严格模式并且第一个参数是nullor undefinedthis则该方法内部将引用Window. 在严格模式下,这是nullor undefined。我在这个 Fiddle上添加了一个活生生的例子。

此外,如果函数根本没有引用,则传入nullundefined不会造成任何伤害this。这可能就是克罗克福德null在他的例子中使用的原因,不要使事情过于复杂。

于 2014-01-13T09:55:06.823 回答