Rus*_*oms 17 javascript partial-application
在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)));
};
};
Run Code Online (Sandbox Code Playgroud)
所以如果你有一个add功能:
var add = function(num1, num2) {
return num1 + num2;
};
add(2, 4); // returns 6
Run Code Online (Sandbox Code Playgroud)
你可以创建一个已经有一个参数的新函数:
var add1 = add.curry(1);
add1(2); // returns 3
Run Code Online (Sandbox Code Playgroud)
这很好.但我想知道的是为什么他设置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)));
};
};
Run Code Online (Sandbox Code Playgroud)
例
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"
Run Code Online (Sandbox Code Playgroud)
如果我尝试做道格拉斯克罗克福德的方式:
myCalc.multiplyPi = myCalc.multiply.curry(Math.PI);
myCalc.multiplyPi(1); // TypeError: Cannot call method 'concat' of undefined
Run Code Online (Sandbox Code Playgroud)
如果我这样做:
myCalc.multiplyPi = myCalc.multiply.myCurry(Math.PI);
myCalc.multiplyPi(1); // returns 3.141592653589793
myCalc.back(); // returns "3.141592653589793 * 1"
Run Code Online (Sandbox Code Playgroud)
但是,我觉得如果道格拉斯·克罗克福德按照自己的方式行事,他可能有充分的理由.我错过了什么?
读者要小心,你是在害怕.
当涉及到JavaScript中的currying,函数,部分应用程序和面向对象时,有很多话要谈.我会尽量保持这个答案尽可能短,但还有很多事要讨论.因此,我将我的文章分为几个部分,在每个部分的最后,我总结了每个部分,供那些急于阅读所有内容的人使用.
我们来谈谈Haskell.在Haskell中,默认情况下每个函数都是curry.例如,我们可以add在Haskell中创建一个函数,如下所示:
add :: Int -> Int -> Int
add a b = a + b
Run Code Online (Sandbox Code Playgroud)
注意类型签名Int -> Int -> Int?它意味着add取一个Int并返回一个类型的函数,而这个函数Int -> Int依次取一个Int并返回一个Int.这允许您轻松地在Haskell中部分应用函数:
add2 :: Int -> Int
add2 = add 2
Run Code Online (Sandbox Code Playgroud)
JavaScript中的相同功能看起来很难看:
function add(a) {
return function (b) {
return a + b;
};
}
var add2 = add(2);
Run Code Online (Sandbox Code Playgroud)
这里的问题是默认情况下JavaScript中的函数不是curry.你需要手动咖喱他们,这是一个痛苦.因此我们使用部分应用程序(aka bind)代替.
第1课:使用 Currying可以更容易地部分应用函数.然而,它仅在默认情况下调用函数的语言中有效(例如Haskell).如果你必须手动咖喱功能,那么最好使用部分应用程序.
Haskell中也存在未计算的函数.它们看起来像"普通"编程语言中的函数:
main = print $ add(2, 3)
add :: (Int, Int) -> Int
add(a, b) = a + b
Run Code Online (Sandbox Code Playgroud)
您可以分别使用Haskell中的uncurry和curry函数将其咖喱形式的函数转换为其未处理的形式,反之亦然.Haskell中一个未经证实的函数仍然只需要一个参数.但是,该参数是多个值(即产品类型)的乘积.
同样,JavaScript中的函数也只占用一个参数(它还不知道它).该论点是一种产品类型.的arguments一个函数内部值是产品类型的一种表现.这通过applyJavaScript中的方法来举例说明,该方法采用产品类型并对其应用函数.例如:
print(add.apply(null, [2, 3]));
Run Code Online (Sandbox Code Playgroud)
你能看到JavaScript中的上述行与Haskell中的以下行之间的相似性吗?
main = print $ add(2, 3)
Run Code Online (Sandbox Code Playgroud)
main如果您不知道它的用途,请忽略该分配.这与手头的主题无关.重要的是(2, 3)Haskell 中的元组[2, 3]与JavaScript中的数组同构.我们从中学到了什么?
的apply在JavaScript功能是一样的功能的应用程序(或$)在Haskell:
($) :: (a -> b) -> a -> b
f $ a = f a
Run Code Online (Sandbox Code Playgroud)
我们使用类型函数a -> b并将其应用于类型的值a以获取类型的值b.但是,由于JavaScript中的所有函数都是默认的,因此apply函数始终将产品类型(即数组)作为其第二个参数.也就是说,type的值a实际上是JavaScript中的产品类型.
第2课: JavaScript中的所有函数只接受一个参数,即产品类型(即arguments值).无论是有意还是偶然,都是一个猜测问题.然而,重要的一点是,您在数学上理解每个函数只需要一个参数.
在数学上,函数被定义为态射:a -> b.它采用type的值a并返回type 的值b.态射只能有一个参数.如果你想要多个参数,那么你可以:
b另一个态射).这是令人讨厌的.哈斯克尔做到了这一点.a为多种类型a的产品(即产品类型).JavaScript做到了这一点.在这两个中,我更喜欢curried函数,因为它们使部分应用变得微不足道.部分应用"未经证实"的功能更复杂.请注意,并不困难,但更复杂.这是我喜欢Haskell而不是JavaScript的原因之一:默认情况下,函数是curry.
我们来看看JavaScript中的一些面向对象的代码.例如:
var oddities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filter(odd).length;
function odd(n) {
return n % 2 !== 0;
}
Run Code Online (Sandbox Code Playgroud)
现在您可能想知道这是面向对象的.它看起来更像功能代码.毕竟你可以在Haskell做同样的事情:
oddities = length . filter odd $ [0..9]
Run Code Online (Sandbox Code Playgroud)
然而,上面的代码是面向对象的.数组文字是一个对象,它有一个filter返回新数组对象的方法.然后我们只需访问length新数组对象.
我们从中学到了什么?面向对象语言中的链接操作与函数语言中的组合函数相同.唯一的区别是功能代码向后读.让我们看看为什么.
在JavaScript中,this参数是特殊的.它与函数的形式参数分开,这就是为什么需要在apply方法中单独指定它的值.因为this在形式参数之前,方法是从左到右链接的.
add.apply(null, [2, 3]); // this comes before the formal parameters
Run Code Online (Sandbox Code Playgroud)
如果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
Run Code Online (Sandbox Code Playgroud)
不是很好吗?那为什么Haskell中的函数会向后读?答案是讨好的.您看到Haskell中的函数也有一个" this"参数.然而,与JavaScript不同,thisHaskell中的参数并不特殊.另外它出现在参数列表的末尾.例如:
filter :: (a -> Bool) -> [a] -> [a]
Run Code Online (Sandbox Code Playgroud)
该filter函数采用谓词函数和this列表,并返回仅包含已过滤元素的新列表.那么为什么this参数会持续?它使部分应用更容易.例如:
filterOdd = filter odd
oddities = length . filterOdd $ [0..9]
Run Code Online (Sandbox Code Playgroud)
在JavaScript中你会写:
Array.prototype.filterOdd = [].filter.myCurry(odd);
var oddities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filterOdd().length;
Run Code Online (Sandbox Code Playgroud)
你现在选哪一个?如果你还在抱怨向后看,那么我有新闻给你.您可以使用"向后应用程序"和"向后组合"向前读取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
Run Code Online (Sandbox Code Playgroud)
现在你拥有两全其美.您的代码向前读取,您可以获得currying的所有好处.
this功能语言中没有出现很多问题:
this参数是专门的.与其他参数不同,您不能简单地将其设置为任意对象.因此,您需要使用call为其指定不同的值this.null为第一个参数bind.同样的call和apply.面向对象编程与此无关this.实际上,您也可以在Haskell中编写面向对象的代码.我甚至可以说Haskell实际上是一种面向对象的编程语言,而且比Java或C++更好.
第3课:函数式编程语言比大多数主流的面向对象编程语言更面向对象.事实上,如果以功能样式编写,JavaScript中的面向对象代码会更好(尽管可读性较差).
JavaScript中面向对象代码的问题是this参数.在我看来,this参数不应该与形式参数有任何不同(Lua得到了正确的答案).问题this是:
this像其他形式参数一样设置.你必须call改用.this为nullin bind.在旁注中我刚刚意识到本文的每个部分都比前一部分更长.因此,我保证尽可能缩短下一个(也是最后一个)部分.
到现在为止你必须已经接受了我认为大多数JavaScript都被破坏了,你应该转而使用Haskell.我喜欢相信道格拉斯·克罗克福德也是一名功能性程序员,他正试图修复JavaScript.
我怎么知道他是一名功能性程序员?他是那样的人:
new关键字(又名Object.create)的功能等价物.如果您还没有这样做,则应停止使用该new关键字.无论如何,我认为Crockford this在curry函数中无效,因为他知道有多糟糕this.除了null在一本名为"JavaScript:The Good Parts"的书中,将它设置为其他任何东西都是亵渎神明的.我认为他一次让世界成为一个更好的地方.
通过废除thisCrockford迫使你停止依赖它.
编辑:正如Bergi所要求的,我将描述一种更有效的方法来编写面向对象的Calculator代码.我们将使用Crockford的curry方法.让我们从multiply和back函数开始:
function multiply(a, b, history) {
return [a * b, [a + " * " + b].concat(history)];
}
function back(history) {
return [history[0], history.slice(1)];
}
Run Code Online (Sandbox Code Playgroud)
如您所见,multiply和back函数不属于任何对象.因此,您可以在任何阵列上使用它们.特别是你的Calculator类只是字符串列表的包装器.因此,您甚至不需要为它创建不同的数据类型.因此:
var myCalc = [];
Run Code Online (Sandbox Code Playgroud)
现在您可以使用Crockford的curry方法进行部分应用:
var multiplyPi = multiply.curry(Math.PI);
Run Code Online (Sandbox Code Playgroud)
接下来,我们将创建一个test函数,multiplyPi并返回到先前的状态:
var test = bindState(multiplyPi.curry(1), function (prod) {
alert(prod);
return back;
});
Run Code Online (Sandbox Code Playgroud)
如果你不喜欢语法,那么你可以切换到LiveScript:
test = do
prod <- bindState multiplyPi.curry 1
alert prod
back
Run Code Online (Sandbox Code Playgroud)
该bindState函数是bind状态monad 的函数.它的定义如下:
function bindState(g, f) {
return function (s) {
var a = g(s);
return f(a[0])(a[1]);
};
}
Run Code Online (Sandbox Code Playgroud)
所以让我们来测试一下:
alert(test(myCalc)[0]);
Run Code Online (Sandbox Code Playgroud)
请参阅此处的演示: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)
Run Code Online (Sandbox Code Playgroud)
请参阅已编译的LiveScript代码的演示:http://jsfiddle.net/5h5R9/1/
那么这个代码对象是如何定向的?维基百科将面向对象的编程定义为:
面向对象编程(OOP)是一种编程范例,它将概念表示为具有数据字段(描述对象的属性)和称为方法的相关过程的"对象".对象通常是类的实例,用于彼此交互以设计应用程序和计算机程序.
根据这个定义,像Haskell这样的函数式编程语言是面向对象的,因为:
Functor是一个超类Applicative.上面的代码也是面向对象的.在这种情况下,对象myCalc只是一个数组.它有两个与之相关的功能:multiply和back.但它没有这些功能.正如您所看到的,"功能"面向对象的代码具有以下优点:
所以我希望有所帮助.
问题是你的解决方案不通用。如果调用者没有将新函数分配给任何对象,或者将其分配给完全不同的对象,您的multiplyPi函数将停止工作:
var multiplyPi = myCalc.multiply.myCurry(Math.PI);
multiplyPi(1); // TypeError: this.history.concat is not a function
Run Code Online (Sandbox Code Playgroud)
因此,Crockford 和您的解决方案都不能确保该函数能够正确使用。那么可以更容易地说该curry函数仅适用于“函数”,而不适用于“方法”,并设置this为null强制执行此操作。不过,我们可能只是猜测,因为克罗克福德在书中没有提到这一点。
如果你问“为什么克罗克福德不使用这个或那个” - 很可能的答案是:“就所证明的问题而言,这并不重要。” Crockford 在函数一章中使用了这个示例。该分章的目的curry是:
针对对象的一般用法进行微调并不是本章的目的。因为它是有问题的,甚至不是不可能的(参见原因1),null如果放在那里的东西可能会引发问题,如果它确实有效或不起作用的话,那么放在那里更有教育意义(尽管对你的情况没有帮助:-)) 。
也就是说,我认为您可以对自己的解决方案充满信心!对于您的情况,没有特别的理由遵循康乐福 (Crockfords) 重置为 的this决定null。但您必须注意,您的解决方案仅在某些情况下有效,并且不是 100% 干净。那么干净的“面向对象”解决方案将要求对象在其自身内部创建其方法的克隆,以确保生成的方法将保留在同一个对象中。
| 归档时间: |
|
| 查看次数: |
1030 次 |
| 最近记录: |