Pengenalan
Mari kita lihat soalan kecil dahulu:
Seseorang menyiarkan soalan dalam kumpulan:
var s = sum(1)(2)(3) ....... Akhirnya makluman keluar sebagai 6
var s = sum(1)(2)(3)(4) ....... Akhirnya makluman keluar sebagai 10
Tanya bagaimana untuk melaksanakan jumlah?
Apabila saya mula-mula melihat tajuk itu, reaksi pertama saya ialah jumlah mengembalikan fungsi, tetapi akhirnya ia tidak dilaksanakan saya telah melihat prinsip yang sama dalam fikiran saya, tetapi saya tidak dapat mengingatinya dengan jelas.
Kemudian, seorang rakan sekerja berkata bahawa ini dipanggil kari,
Kaedah pelaksanaan lebih bijak:
function sum(x){ var y = function(x){ return sum(x+y) } y.toString = y.valueOf = function(){ return x; } return y; }
Mari kita lihat lebih dekat tentang karipap~
Apa itu kari?
Kari ialah proses penukaran yang mengubah fungsi yang menerima berbilang parameter kepada fungsi yang menerima satu parameter (nota: parameter pertama fungsi asal jika parameter lain diperlukan, kembali untuk menerima fungsi baharu yang mengambil parameter yang tinggal dan mengembalikan hasil.
Saya rasa karipap kedengaran agak mudah apabila kita menyatakannya seperti itu. Bagaimanakah ia dilaksanakan dalam JavaScript?
Katakan kita ingin menulis fungsi yang menerima 3 parameter.
var sendMsg = function (from, to, msg) { alert(["Hello " + to + ",", msg, "Sincerely,", "- " + from].join("\n")); };
Sekarang, katakan kita mempunyai fungsi kari yang menukar fungsi JavaScript tradisional kepada fungsi kari:
var sendMsgCurried = curry(sendMsg); // returns function(a,b,c) var sendMsgFromJohnToBob = sendMsgCurried("John")("Bob"); // returns function(c) sendMsgFromJohnToBob("Come join the curry party!"); //=> "Hello Bob, Come join the curry party! Sincerely, - John"
Karipap manual
Dalam contoh di atas, kita mengandaikan bahawa kita mempunyai fungsi kari yang misteri. Saya akan melaksanakan fungsi sedemikian, tetapi buat masa ini, mari kita lihat dahulu mengapa fungsi sedemikian sangat diperlukan.
Contohnya, mengurut fungsi secara manual tidaklah sukar, tetapi ia agak bertele-tele:
// uncurried var example1 = function (a, b, c) { // do something with a, b, and c }; // curried var example2 = function(a) { return function (b) { return function (c) { // do something with a, b, and c }; }; };
Dalam JavaScript, walaupun anda tidak menyatakan semua parameter fungsi, fungsi itu tetap akan dipanggil. Ini adalah ciri JavaScript yang sangat berguna, tetapi ia menimbulkan masalah untuk kari.
Ideanya ialah setiap fungsi ialah fungsi dengan satu dan hanya satu parameter. Jika anda ingin mempunyai berbilang parameter, anda mesti menentukan satu siri fungsi bersarang antara satu sama lain. benci! Melakukan ini sekali atau dua kali adalah baik, tetapi apabila anda perlu menentukan fungsi yang memerlukan banyak parameter dengan cara ini, ia menjadi agak bertele-tele dan sukar dibaca. (Tetapi jangan risau, saya akan memberitahu anda cara segera)
Sesetengah bahasa pengaturcaraan berfungsi, seperti Haskell dan OCaml, mempunyai fungsi kari terbina dalam sintaksnya. Dalam bahasa ini, sebagai contoh, setiap fungsi ialah fungsi yang mengambil satu hujah, dan hanya satu hujah. Anda mungkin berfikir bahawa sekatan ini melebihi faedah, tetapi sintaks bahasa itu adalah apa itu, sekatan ini hampir tidak dapat dilihat.
Sebagai contoh, dalam OCaml, anda boleh mentakrifkan contoh di atas dalam dua cara:
let example1 = fun a b c -> // (* do something with a, b, c *) let example2 = fun a -> fun b -> fun c -> // (* do something with a, b, c *)
Mudah untuk melihat bagaimana kedua-dua contoh ini serupa dengan dua contoh di atas.
Walau bagaimanapun, perbezaannya ialah sama ada perkara yang sama dilakukan dalam OCaml. OCaml, tiada fungsi dengan berbilang parameter. Walau bagaimanapun, mengisytiharkan berbilang parameter dalam satu baris ialah "jalan pintas" untuk menyarangkan fungsi parameter tunggal.
Begitu juga, kami menjangkakan bahawa memanggil fungsi kari akan secara sintaksis serupa dengan memanggil fungsi berbilang parameter dalam OCaml. Kami mengharapkan untuk memanggil fungsi di atas seperti ini:
example1 foo bar baz example2 foo bar baz
Dalam JavaScript, kami mengambil pendekatan yang berbeza dengan ketara:
example1(foo, bar, baz); example2(foo)(bar)(baz);
Dalam bahasa seperti OCaml, kari terbina dalam. Dalam JavaScript, walaupun kari boleh dilakukan (fungsi tertib tinggi), ia menyusahkan dari segi sintaksis. Inilah sebabnya kami memutuskan untuk menulis fungsi kari untuk melakukan perkara yang membosankan ini untuk kami dan menjadikan kod kami lebih mudah.
Buat fungsi pembantu kari
Secara teorinya kami berharap untuk mempunyai cara yang mudah untuk menukar fungsi JavaScript lama biasa (berbilang parameter) kepada fungsi kari sepenuhnya.
Idea ini bukan unik kepada saya, orang lain telah melaksanakannya, seperti fungsi .autoCurry() dalam perpustakaan wu.js (walaupun perkara yang anda bimbangkan ialah pelaksanaan kami sendiri).
Mula-mula, mari buat fungsi penolong mudah .sub_curry:
function sub_curry(fn /*, variable number of args */ ) { var args = [].slice.call(arguments, 1); return function () { return fn.apply(this, args.concat(toArray(arguments))); }; }
Mari kita luangkan sedikit masa untuk melihat fungsi ini. Cukup mudah. sub_curry menerima fungsi fn sebagai argumen pertamanya, diikuti dengan sebarang bilangan argumen input. Apa yang dikembalikan ialah fungsi. Fungsi ini mengembalikan hasil pelaksanaan fn.apply Urutan parameter menggabungkan parameter yang pada mulanya dihantar ke fungsi, ditambah dengan parameter yang dihantar apabila fn dipanggil.
Lihat contoh:
var fn = function(a, b, c) { return [a, b, c]; }; // these are all equivalent fn("a", "b", "c"); sub_curry(fn, "a")("b", "c"); sub_curry(fn, "a", "b")("c"); sub_curry(fn, "a", "b", "c")(); //=> ["a", "b", "c"]
Jelas sekali, ini bukan yang kita mahu, tetapi nampaknya agak kari. Sekarang kita akan mentakrifkan fungsi kari:
function curry(fn, length) { // capture fn's # of parameters length = length || fn.length; return function () { if (arguments.length < length) { // not all arguments have been specified. Curry once more. var combined = [fn].concat(toArray(arguments)); return length - arguments.length > 0 ? curry(sub_curry.apply(this, combined), length - arguments.length) : sub_curry.call(this, combined ); } else { // all arguments have been specified, actually call function return fn.apply(this, arguments); } }; }
Fungsi ini menerima dua parameter, satu fungsi dan bilangan parameter yang akan "dikari". Parameter kedua adalah pilihan Jika diabaikan, sifat Function.prototype.length digunakan secara lalai, hanya untuk memberitahu anda berapa banyak parameter yang ditakrifkan oleh fungsi ini.
Akhirnya, kita boleh menunjukkan tingkah laku berikut:
var fn = curry(function(a, b, c) { return [a, b, c]; }); // these are all equivalent fn("a", "b", "c"); fn("a", "b", "c"); fn("a", "b")("c"); fn("a")("b", "c"); fn("a")("b")("c"); //=> ["a", "b", "c"]
我知道你在想什么…
等等…什么?!
难道你疯了?应该是这样!我们现在能够在JavaScript中编写柯里化函数,表现就如同OCaml或者Haskell中的那些函数。甚至,如果我想要一次传递多个参数,我可以向我从前做的那样,用逗号分隔下参数就可以了。不需要参数间那些丑陋的括号,即使是它是柯里化后的。
这个相当有用,我会立即马上谈论这个,可是首先我要让这个Curry函数前进一小步。
柯里化和“洞”(“holes”)
尽管柯里化函数已经很牛了,但是它也让你必须花费点小心思在你所定义函数的参数顺序上。终究,柯里化的背后思路就是创建函数,更具体的功能,分离其他更多的通用功能,通过分步应用它们。
当然这个只能工作在当最左参数就是你想要分步应用的参数!
为了解决这个,在一些函数式编程语言中,会定义一个特殊的“占位变量”。通常会指定下划线来干这事,如过作为一个函数的参数被传入,就表明这个是可以“跳过的”。是尚待指定的。
这是非常有用的,当你想要分步应用(partially apply)一个特定函数,但是你想要分布应用(partially apply)的参数并不是最左参数。
举个例子,我们有这样的一个函数:
var sendAjax = function (url, data, options) { /* ... */ }
也许我们想要定义一个新的函数,我们部分提供SendAjax函数特定的Options,但是允许url和data可以被指定。
当然了,我们能够相当简单的这样定义函数:
var sendPost = function (url, data) { return sendAjax(url, data, { type: "POST", contentType: "application/json" }); };
或者,使用使用约定的下划线方式,就像下面这样:
var sendPost = sendAjax( _ , _ , { type: "POST", contentType: "application/json" });
注意两个参数以下划线的方式传入。显然,JavaScript并不具备这样的原生支持,于是我们怎样才能这样做呢?
回过头让我们把curry函数变得智能一点…
首先我们把我们的“占位符”定义成一个全局变量。
var _ = {};
我们把它定义成对象字面量{},便于我们可以通过===操作符来判等。
不管你喜不喜欢,为了简单一点我们就使用_来做“占位符”。现在我们就可以定义新的curry函数,就像下面这样:
function curry (fn, length, args, holes) { length = length || fn.length; args = args || []; holes = holes || []; return function(){ var _args = args.slice(0), _holes = holes.slice(0), argStart = _args.length, holeStart = _holes.length, arg, i; for(i = 0; i < arguments.length; i++) { arg = arguments[i]; if(arg === _ && holeStart) { holeStart--; _holes.push(_holes.shift()); // move hole from beginning to end } else if (arg === _) { _holes.push(argStart + i); // the position of the hole. } else if (holeStart) { holeStart--; _args.splice(_holes.shift(), 0, arg); // insert arg at index of hole } else { _args.push(arg); } } if(_args.length < length) { return curry.call(this, fn, length, _args, _holes); } else { return fn.apply(this, _args); } } }
实际代码还是有着巨大不同的。 我们这里做了一些关于这些“洞”(holes)参数是什么的记录。概括而言,运行的职责是相同的。
展示下我们的新帮手,下面的语句都是等价的:
var f = curry(function(a, b, c) { return [a, b, c]; }); var g = curry(function(a, b, c, d, e) { return [a, b, c, d, e]; }); // all of these are equivalent f("a","b","c"); f("a")("b")("c"); f("a", "b", "c"); f("a", _, "c")("b"); f( _, "b")("a", "c"); //=> ["a", "b", "c"] // all of these are equivalent g(1, 2, 3, 4, 5); g(_, 2, 3, 4, 5)(1); g(1, _, 3)(_, 4)(2)(5); //=> [1, 2, 3, 4, 5]
疯狂吧?!
我为什么要关心?柯里化能够怎么帮助我?
你可能会停在这儿思考…
这看起来挺酷而且…但是这真的能帮助我编写更好的代码?
这里有很多原因关于为什么函数柯里化是有用的。
函数柯里化允许和鼓励你分隔复杂功能变成更小更容易分析的部分。这些小的逻辑单元显然是更容易理解和测试的,然后你的应用就会变成干净而整洁的组合,由一些小单元组成的组合。
为了给一个简单的例子,让我们分别使用Vanilla.js, Underscore.js, and “函数化方式” (极端利用函数化特性)来编写CSV解析器。
Vanilla.js (Imperative)
//+ String -> [String] var processLine = function (line){ var row, columns, j; columns = line.split(","); row = []; for(j = 0; j < columns.length; j++) { row.push(columns[j].trim()); } }; //+ String -> [[String]] var parseCSV = function (csv){ var table, lines, i; lines = csv.split("\n"); table = []; for(i = 0; i < lines.length; i++) { table.push(processLine(lines[i])); } return table; }; Underscore.js //+ String -> [String] var processLine = function (row) { return _.map(row.split(","), function (c) { return c.trim(); }); }; //+ String -> [[String]] var parseCSV = function (csv){ return _.map(csv.split("\n"), processLine); };
函数化方式
//+ String -> [String] var processLine = compose( map(trim) , split(",") ); //+ String -> [[String]] var parseCSV = compose( map(processLine) , split("\n") );
所有这些例子功能上是等价的。我有意的尽可能的简单的编写这些。
想要达到某种效果是很难的,但是主观上这些例子,我真的认为最后一个例子,函数式方式的,体现了函数式编程背后的威力。
关于curry性能的备注
一些极度关注性能的人可以看看这里,我的意思是,关注下所有这些额外的事情?
通常,是这样,使用柯里化会有一些开销。取决于你正在做的是什么,可能会或不会,以明显的方式影响你。也就是说,我敢说几乎大多数情况,你的代码的拥有性能瓶颈首先来自其他原因,而不是这个。
有关性能,这里有一些事情必须牢记于心: