在克罗克福德的“咖喱”方法中,“this”无效吗?

时间:2014-01-08 17:24:46

标签: javascript partial-application

在Douglas Crockford的书“Javascript:The Good Parts”中,他提供了一个curry方法的代码,该方法接受一个函数和参数,并使用已经添加的参数返回该函数(显然,这不是真的{{3 },但是what "curry" means)的一个例子。这是我修改过的代码,以便在没有其他自定义代码的情况下工作:

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?预期的行为是不是curried方法与原始方法相同,包括相同的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)));
  };
};

示例

"partial application"

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"

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

4 个答案:

答案 0 :(得分:5)

读者要小心,你是在吓唬。

在JavaScript中讨论currying,函数,部分应用程序和面向对象时,还有很多东西需要讨论。我会尽量保持这个答案尽可能短,但需要讨论很多。因此,我将我的文章分为几个部分,在每个部分的最后,我总结了每个部分,供那些急于阅读所有内容的人使用。


1。咖喱或不咖喱

让我们谈谈Haskell。在Haskell中,默认情况下每个函数都是curry。例如,我们可以在Haskell中创建一个add函数,如下所示:

add :: Int -> Int -> Int
add a b = a + b

注意类型签名Int -> Int -> Int?这意味着add需要Int并返回Int -> Int类型的函数,而Int依次为Int并返回add2 :: Int -> Int add2 = add 2 。这允许您轻松地在Haskell中部分应用函数:

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

这里的问题是默认情况下JavaScript中的函数不是curry。你需要手动咖喱他们,这是一个痛苦。因此,我们使用部分应用程序(又名bind)。

第1课:使用Currying可以更容易地部分应用函数。然而,它仅在默认情况下调用函数的语言中有效(例如Haskell)。如果您必须手动咖喱功能,那么最好使用部分应用程序。


2。函数的结构

Haskell中也存在未经验证的函数。它们看起来像" normal"编程语言:

arguments

您可以分别使用Haskell中的uncurrycurry函数将其咖喱形式的函数转换为其未处理的形式,反之亦然。 Haskell中一个未经证实的函数仍然只需要一个参数。然而,该论证是多个值的乘积(即product type)。

同样,JavaScript中的函数也只需要一个参数(它还不知道它)。该论点是一种产品类型。函数内的apply值是该产品类型的表现形式。这可以通过JavaScript中的print(add.apply(null, [2, 3])); 方法来举例说明,该方法采用产品类型并对其应用函数。例如:

main = print $ add(2, 3)

你能看到JavaScript中的上一行与Haskell中的以下行之间的相似性吗?

main

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

JavaScript中的$函数与Haskell中的函数应用程序(或($) :: (a -> b) -> a -> b f $ a = f a )相同:

a -> b

我们使用a类型的函数并将其应用于类型b的值以获取类型apply的值。但是,由于默认情况下JavaScript中的所有函数都不受限制,因此a函数始终将产品类型(即数组)作为其第二个参数。也就是说,类型arguments的值实际上是JavaScript中的产品类型。

第2课: JavaScript中的所有函数只接受一个作为产品类型的参数(即a -> b值)。无论是有意还是偶然,都是一个猜测问题。然而,重要的一点是,您在数学上理解每个函数只需要一个参数。

数学上,函数定义为morphisma。它采用类型b的值并返回类型b的值。态射只能有一个参数。如果你想要多个参数,那么你可以:

  1. 返回另一个态射(即a是另一个态射)。这是令人讨厌的。 Haskell就是这么做的。
  2. a定义为多种类型的产品(即var oddities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filter(odd).length; function odd(n) { return n % 2 !== 0; } 是产品类型)。 JavaScript就是这么做的。
  3. 在这两个中,我更喜欢curried函数,因为它们使部分应用变得微不足道。部分应用"未发现"功能更复杂。请注意,并不困难,但更复杂。这是我喜欢Haskell而不是JavaScript的原因之一:默认情况下函数是curry。


    3。为什么OOP不重要

    让我们来看看JavaScript中的一些面向对象的代码。例如:

    oddities = length . filter odd $ [0..9]
    

    现在你可能想知道这个面向对象是怎么回事。它看起来更像功能代码。毕竟你可以在Haskell做同样的事情:

    filter

    然而,上面的代码是面向对象的。数组文字是一个对象,它有一个方法length,它返回一个新的数组对象。然后我们只需访问新数组对象的this

    我们从中学到了什么?面向对象语言中的链接操作与函数语言中的组合函数相同。唯一的区别是功能代码向后读。让我们看看为什么。

    在JavaScript中,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 在形式参数之后出现,则上述代码可能读作:

    this

    不是很好吗?那为什么Haskell中的函数会向后读?答案是讨好的。你看到Haskell中的函数也有一个" this"参数。但是与JavaScript不同,Haskell中的filter :: (a -> Bool) -> [a] -> [a] 参数并不特殊。另外它出现在参数列表的末尾。例如:

    filter

    this函数采用谓词函数和this列表,并返回仅包含已过滤元素的新列表。那么为什么filterOdd = filter odd oddities = length . filterOdd $ [0..9] 参数会持久?它使部分应用更容易。例如:

    Array.prototype.filterOdd = [].filter.myCurry(odd);
    var oddities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filterOdd().length;
    

    在JavaScript中你会写:

    ($>) :: a -> (a -> b) -> b
    a $> f = f a
    
    (>>>) :: (a -> b) -> (b -> c) -> (a -> c)
    f >>> g = g . f
    
    oddities = [0..9] $> filter odd >>> length
    

    现在你会选择哪一个?如果您还在抱怨向后阅读,那么我会为您提供新闻。您可以使用"后向应用程序"来向前读取Haskell代码。 "落后的作文"如下:

    this

    现在你拥有两全其美。你的代码向前读取,你可以获得currying的所有好处。

    this存在很多问题,这些问题不会出现在函数式语言中:

    1. call参数是专门的。与其他参数不同,您不能简单地将其设置为任意对象。因此,您需要使用thisnull指定不同的值。
    2. 如果您想在JavaScript中部分应用函数,则需要指定bind作为call的第一个参数。同样适用于applythis
    3. 面向对象的编程与this无关。实际上,您也可以在Haskell中编写面向对象的代码。我甚至可以说Haskell实际上是一种面向对象的编程语言,而且比Java或C ++更好。

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

      JavaScript中面向对象代码的问题是this参数。在我看来,this参数不应该与形式参数区别对待(Lua得到了这个权利)。 this的问题在于:

      1. 没有办法像其他形式参数那样设置call。您必须使用this代替。
      2. 如果您只希望部分应用某项功能,则必须在null中将bind设为new
      3. 在旁注中我刚刚意识到本文的每个部分都比前一部分更长。因此,我保证尽可能缩短下一个(也是最后一个)部分。


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

        到目前为止,您必须已经认识到我认为大多数JavaScript都已损坏,您应该转而使用Haskell。我喜欢相信道格拉斯·克罗克福德也是一名功能性程序员,而且他正试图修复JavaScript。

        我如何知道他是一名功能性程序员?他是那个人:

        1. 推广Object.create关键字(a.k.a this)的功能等价物。如果您还没有这样做,那么您应该stop using the new keyword
        2. 尝试向JavaScript社区解释monads and gonads的概念。
        3. 无论如何,我认为Crockford在curry函数中使this无效,因为他知道null有多糟糕。在题为" JavaScript:The Good Parts"的书中将它设置为this以外的任何内容将是亵渎神明的。我认为他一次让这个世界成为一个更好的地方。

          通过废除Calculator Crockford迫使你不再依赖它。


          编辑:正如Bergi所要求的那样,我将描述一种更有效的方法来编写面向对象的curry代码。我们将使用Crockford的multiply方法。让我们从backfunction multiply(a, b, history) { return [a * b, [a + " * " + b].concat(history)]; } function back(history) { return [history[0], history.slice(1)]; } 函数开始:

          multiply

          正如您所看到的,backCalculator功能并不属于任何对象。因此,您可以在任何阵列上使用它们。特别是你的var myCalc = []; 类只是字符串列表的包装器。因此,您甚至不需要为它创建不同的数据类型。因此:

          curry

          现在您可以使用Crockford的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
          

          如果您不喜欢语法,则可以切换到LiveScript

          bindState

          bind函数是状态monad的function bindState(g, f) { return function (s) { var a = g(s); return f(a[0])(a[1]); }; } 函数。它的定义如下:

          alert(test(myCalc)[0]);
          

          让我们进行测试:

          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)
          

          请参阅此处的演示:http://jsfiddle.net/5h5R9/

          如果使用LiveScript编写,整个程序将更简洁,如下所示:

          Functor

          请参阅已编译的LiveScript代码的演示:http://jsfiddle.net/5h5R9/1/

          那么这个代码对象是如何定向的?维基百科将object-oriented programming定义为:

            

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

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

          1. 在Haskell中,我们将概念表示为algebraic data types,它们基本上是&#34;类固醇上的对象&#34;。 ADT具有一个或多个可能具有零个或多个数据字段的构造函数。
          2. Haskell中的ADT具有相关功能。然而,与主流的面向对象编程语言不同,ADT不具备这些功能。相反,这些功能专注于ADT。这实际上是一件好事,因为ADT可以添加更多方法。在传统的OOP语言中,如Java和C ++,它们都是封闭的。
          3. ADT可以成为与Java中的接口类似的类型类的实例。因此,您仍然具有继承,方差和子类型多态性,但其形式要少得多。例如,ApplicativemyCalc的超类。
          4. 上面的代码也是面向对象的。在这种情况下,对象是multiply,它只是一个数组。它有两个与之关联的功能:back和{{1}}。但它并不拥有这些功能。你可以看到&#34;功能&#34;面向对象的代码具有以下优点:

            1. 对象没有自己的方法。因此,将新功能与对象联系起来很容易。
            2. 通过currying使部分应用变得简单。
            3. 它促进了通用编程。
            4. 所以我希望有所帮助。

答案 1 :(得分:4)

  

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

没有什么理由。可能他想要简化,并且大多数有意义被咖喱或部分应用的函数不是使用this的OOP方法。在更具功能性的样式中,附加到的history数组将是函数的另一个参数(甚至可能是返回值)。

  

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

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

对于那些,您可能会查看部分应用程序的bind method个Function对象,包括特定的this - 值。

答案 2 :(得分:4)

原因1 - 不容易提供一般解决方案

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

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

因此,Crockford和您的解决方案都不能确保该功能得到正确使用。然后,可能更容易说curry函数仅适用于“函数”,而不适用于“方法”,并将this设置为null以强制执行此操作。我们可能只会猜测,因为Crockford在书中没有提到这一点。

原因2 - 正在解释功能

如果你问“为什么Crockford没有使用这个或那个” - 很可能的答案是:“对于所展示的问题,这并不重要。” Crockford在这个例子中使用章节功能。分章curry的目的是:

  • 表示功能是您可以创建和操作的对象
  • 演示闭包的另一种用法
  • 显示如何操纵参数。

将此用于对象的一般用法并不是本章的目的。因为它是有问题的,甚至不可能(参见原因1),如果把那些某些东西放在那里可能会引发问题,如果它实际上有效,那就更有教育意义了({1}}虽然在你的情况下没有帮助: - ))。

结论

那就是说,我认为你对你的解决方案充满信心!在您的情况下,没有特别的理由遵循Crockfords决定将null重置为this您必须意识到,您的解决方案仅在某些情况下有效,并且不是100%干净。然后清理“面向对象”的解决方案将要求对象在其自身内部创建其方法的克隆,以确保结果方法将保留在同一对象中。

答案 3 :(得分:2)

来自MDN

  

thisArg为乐趣调用提供的值。请注意这一点   可能不是方法看到的实际值:如果方法是a   在非严格模式代码中的函数,null和undefined将被替换   使用全局对象,原始值将被加框。

因此,如果方法处于非严格模式且第一个参数为nullundefined,则该方法内的this将引用Window。在严格模式下,这是nullundefined。我在this Fiddle上添加了一个实例。

此外,如果函数根本没有引用null,则传入undefinedthis不会造成任何伤害。这可能就是为什么Crockford在他的例子中使用null,以免使事情过于复杂。