カリー化を学ぶ前に、まず次のコードを見てみましょう:
var f1 = function(x){ return f(x); }; f1(x);
関数 f1 </ code> と <code>f
は同等です。var f1 = f;
をこのようなレイヤーでラップする必要はありません。 f1
和f
是等效的,我们直接令var f1 = f;
就行了,完全没有必要包裹那么一层。
但是,下面一段代码就未必能够看得出问题来了:
var getServerStuff = function(callback){ return ajaxCall(function(json){ return callback(json); }); };
这是我摘自《JS函数式编程指南》中的一段代码,实际上,利用上面的规则,我们可以得出callback
与函数
function(json){return callback(json);};
是等价的,所以函数可以化简为:
var getServerStuff = function(callback){ return ajaxCall(callback); };
继续化简:
var getServerStuff = ajaxCall;
如此一来,我们发现那么长一段程序都白写了。
函数既可以当参数,又可以当返回值,是高阶函数的一个重要特性,但是稍不留神就容易踩到坑里。
言归正传,什么是函数柯里化?函数柯里化(curry)就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。听得很绕口,其实很简单,其实就是将函数的变量拆分开来调用:f(x,y,z) -> f(x)(y)(z)
。
对于最开始的例子,按照如下实现,要传入两个参数,f1
调用方式是f1(f,x)
。
var f1 = function(f,x){ return f(x); };
注意,由于f
是作为一个函数变量传入,所以f1
变成了一个新的函数。
我们将f1
变化一下,利用闭包可以写成如下形式,则f1
调用方式变成了f1(f)(x)
,而且得到的结果完全一样。这就完成了f1
的柯里化。
var f1 = function(f){ return function(x){ return f(x); } }; var f2 = f1(f); f2(x);
其实这个例子举得不恰当,细心的同学可能会发现,f1
虽然是一个新函数,但是f2
和f
是完全等效的,绕了半天,还是绕回来了。
这里有一个很经典的例子:
['11', '11', '11'].map(parseInt) //[ 11, NaN, 3 ] ['11', '11', '11'].map(f1(parseInt)) //[ 11, 11, 11 ]
由于parseInt
接受两个参数,所以直接调用会有进制转换的问题,参考“不愿相离”的文章。
var f2 = f1(parseInt)
,f2
让parseInt
由原来的接受两个参数变成了只接受一个参数的新函数,从而解决这个进制转换问题。通过我们的f1
包裹以后就能够运行出正确的结果了。
有同学觉得这个不算柯里化的应用,我觉得还是算吧,各位同学可以一起来讨论下。
如果说上一节的例子中,我们不是直接运行f(x)
,而是把函数f
当做一个参数,结果会怎样呢?我们来看下面这个例子:
假设f1
返回函数g
,g
的作用域指向xs
,函数f
作为g
的参数。最终我们可以写成如下形式:
var f1 = function(f,xs){ return g.call(xs,f); };
实际上,用f1
来替代g.call(xxx)
的做法叫反柯里化。例如:
var forEach = function(xs,f){ return Array.prototype.forEach.call(xs,f); }; var f = function(x){console.log(x);}; var xs = {0:'peng',1:'chen',length:2}; forEach(xs,f);
反curring就是把原来已经固定的参数或者this上下文等当作参数延迟到未来传递。
它能够在很大程度上简化函数,前提是你得习惯它。
抛开反柯里化,如果我们要柯里化f1
怎么办?
使用闭包,我们可以写成如下形式:
var f1 = function(f){ return function(xs){ return g.call(xs,f); } }; var f2 = f1(f); f2(xs);
把f
传入f1
中,我们就可以得到f2
这个新函数。
只传给函数一部分参数通常也叫做局部调用(partial application),能够大量减少样板文件代码(boilerplate code)。
当然,函数f1
传入的两个参数不一定非得包含函数+非函数,可能两个都是函数,也可能两个都是非函数。
我个人觉得柯里化并非是必须的,而且不熟悉的同学阅读起来可能会遇到麻烦,但是它能帮助我们理解JS中的函数式编程,更重要的是,我们以后在阅读类似的代码时,不会感到陌生。知乎上罗宸同学讲的挺好:
并非“柯里化”对函数式编程有意义。而是,函数式编程在把函数当作一等公民的同时,就不可避免的会产生“柯里化”这种用法。所以它并不是因为“有什么意义”才出现的。当然既然存在了,我们自然可以探讨一下怎么利用这种现象。
练习:
// 通过局部调用(partial apply)移除所有参数 var filterQs = function(xs) { return filter(function(x){ return match(/q/i, x); }, xs); }; //这两个函数原题没有,是我自己加的 var filter = function(f,xs){ return xs.filter(f); }; var match = function(what,x){ return x.match(what); };
分析:函数filterQs
的作用是:传入一个字符串数组,过滤出包含’q'的字符串,并组成一个新的数组返回。
我们可以通过如下步骤得到函数filterQs
var filter = function(f){ return function (xs) { return xs.filter(f); } };
callback
は関数 🎜var match = function(what){ return function(x){ return x.match(what); } }; var match2 = match(/q/i);
var filterQs = filter(match2); var xs = ['q','test1','test2']; filterQs(xs);
function add( seed ) { function retVal( later ) { return add( seed + later ); } retVal.toString = function() { return seed; }; return retVal; } console.log(add(1)(2)(3).toString()); // 6
f(x,y,z) -> f(x)(y)(z; )</コード> 。 🎜🎜最初の例では、次のように実装します。2 つのパラメーターを渡す必要があります。<code>f1
の呼び出しメソッドは f1(f,x)
です。 🎜var compose = function(f,g) { return function(x) { return f(g(x)); }; }; var f1 = compose(f,g); f1(x);
f
が関数変数として渡されるため、f1
が新しい関数になることに注意してください。 🎜🎜f1
を変更し、クロージャを使用して次の形式で記述すると、f1
の呼び出しメソッドは次のようになります。 < code>f1(f)(x) と実行すると、まったく同じ結果が得られます。これで f1
のカリー化が完了しました。 🎜// 非 pointfree,因为提到了数据:name var initials = function (name) { return name.split(' ').map(compose(toUpperCase, head)).join('. '); }; // pointfree var initials = compose(join('. '), map(compose(toUpperCase, head)), split(' ')); initials("hunter stockton thompson"); // 'H. S. T'
f1
は新しい関数ですが、f2
と f
は完全に新しい関数です。かなり遠回りしたのに、また戻ってきました。 🎜🎜これは非常に古典的な例です: 🎜rrreee🎜 parseInt
は 2 つのパラメータを受け入れるため、直接呼び出すと 16 進数変換の問題が発生します。記事「分割したくない」を参照してください。 🎜🎜var f2 = f1(parseInt)
、f2
は、parseInt
を 2 つのパラメーターを受け入れるものから、解決するパラメーター関数を 1 つだけ受け入れる新しいものに変更します。この16進数変換の問題。 f1
パッケージを渡すと、正しい結果を実行できるようになります。 🎜🎜これはカレーのような申請ではないと考える生徒もいます。生徒全員で話し合うべきだと思います。 🎜🎜3. 関数のカリー化についてのさらなる考察🎜🎜前のセクションの例では、
f(x)
を直接実行する代わりに、関数 f< を置きます。 /code> をパラメータとして使用すると、結果はどうなりますか?次の例を見てみましょう: 🎜🎜 <code>f1
が関数 g
を返し、g
のスコープが xs
を指しているとします。 >、関数 f
は g
へのパラメータとして機能します。最後に、次の形式で書くことができます: 🎜rrreee🎜 実際、f1
を使用して g.call(xxx)
を置き換えることは、デカリングと呼ばれます。例: 🎜rrreee🎜アンチカリングとは、元の固定パラメータまたはこのコンテキストをパラメータとして将来に延期することです。🎜 アンチカリーはさておき、
慣れれば機能を大幅に簡素化できます。 🎜
f1
をカリーにしたい場合はどうすればよいでしょうか? 🎜🎜クロージャを使用すると、次の形式で書くことができます: 🎜rrreee🎜 f
を f1
に渡すと、新しい f2
を取得できます。関数。 🎜🎜パラメーターの一部のみを関数に渡すことは通常、部分アプリケーションと呼ばれ、定型コードを大幅に削減できます。 🎜🎜 もちろん、関数
f1
によって渡される 2 つのパラメーターには、必ずしも関数 + 非関数が含まれる必要はありません。両方とも関数である場合もあれば、両方とも非関数である場合もあります。 🎜🎜個人的にはカリー化は必要ないと感じており、カリー化に慣れていない学生は読むのが難しいかもしれませんが、JS での関数型プログラミングを理解するのに役立ちます。さらに重要なことに、将来同様の記事を読むときにコーディングするときに役立ちます。 , 違和感を感じることはありません。 Zhihu の Luo Chen は次のようにうまく言っています: 🎜🎜 関数型プログラミングにとって「カリー化」が意味があるというわけではありません。むしろ、関数型プログラミングは関数を第一級市民として扱いますが、必然的に「カリー」の使用法が生じます。つまり、「意味があるから」という理由ではありません。もちろん、それが存在する以上、この現象をどのように利用するかを自然に模索することができます。 🎜🎜演習: 🎜rrreee🎜分析: 関数
filterQs
の機能は、文字列配列を渡し、「q」を含む文字列をフィルターで除外し、新しい配列を形成して を返すことです。 🎜🎜次の手順で関数 filterQs
を取得できます: 🎜a. filter
传入的两个参数,第一个是回调函数,第二个是数组,filter
主要功能是根据回调函数过滤数组。我们首先将filter
函数柯里化:
var filter = function(f){ return function (xs) { return xs.filter(f); } };
b. 其次,filter
函数传入的回调函数是match
,match
的主要功能是判断每个字符串是否匹配what
这个正则表达式。这里我们将match
也柯里化:
var match = function(what){ return function(x){ return x.match(what); } }; var match2 = match(/q/i);
创建匹配函数match2
,检查字符串中是否包含字母q。
c. 把match2
传入filter
中,组合在一起,就形成了一个新的函数:
var filterQs = filter(match2); var xs = ['q','test1','test2']; filterQs(xs);
从这个示例中我们也可以体会到函数柯里化的强大。所以,柯里化还有一个重要的功能:封装不同功能的函数,利用已有的函数组成新的函数。
函数柯里化还有一种有趣的形式,就是函数可以在闭包中调用自己,类似于函数递归调用。如下所示:
function add( seed ) { function retVal( later ) { return add( seed + later ); } retVal.toString = function() { return seed; }; return retVal; } console.log(add(1)(2)(3).toString()); // 6
add
函数返回闭包retVal
,在retVal
中又继续调用add
,最终我们可以写成add(1)(2)(3)(...)
这样柯里化的形式。
关于这段代码的解答,知乎上的李宏训同学回答地很好:
每调用一次add函数,都会返回retValue函数;调用retValue函数会调用add函数,然后还是返回retValue函数,所以调用add的结果一定是返回一个retValue函数。add函数的存在意义只是为了提供闭包,这个类似的递归调用每次调用add都会生成一个新的闭包。
函数组合是在柯里化基础上完成的:
var compose = function(f,g) { return function(x) { return f(g(x)); }; }; var f1 = compose(f,g); f1(x);
将传入的函数变成两个,通过组合的方式返回一个新的函数,让代码从右向左运行,而不是从内向外运行。
函数组合和柯里化有一个好处就是pointfree。
pointfree 模式指的是,永远不必说出你的数据。它的意思是说,函数无须提及将要操作的数据是什么样的。一等公民的函数、柯里化(curry)以及组合协作起来非常有助于实现这种模式。
// 非 pointfree,因为提到了数据:name var initials = function (name) { return name.split(' ').map(compose(toUpperCase, head)).join('. '); }; // pointfree var initials = compose(join('. '), map(compose(toUpperCase, head)), split(' ')); initials("hunter stockton thompson"); // 'H. S. T'
以上がJavaScript 関数のカリー化に関する考え方の詳細な紹介の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。