Maison > Article > interface Web > Une analyse approfondie des compétences Currying de fonctions dans JavaScript_javascript
Présentation
Regardons d'abord une petite question :
Quelqu'un a posté une question dans le groupe :
var s = sum(1)(2)(3) ....... Finalement, les alertes sont 6
var s = sum(1)(2)(3)(4) ....... Finalement, les alertes sont 10
Demandez comment mettre en œuvre la somme ?
Quand j'ai vu le titre pour la première fois, ma première réaction a été que sum renvoyait une fonction, mais il n'a finalement pas été mis en œuvre. J'avais vu un principe similaire dans mon esprit, mais je ne m'en souvenais pas clairement.
Plus tard, un collègue a dit que cela s'appelait du curry,
La méthode de mise en œuvre est plus astucieuse :
function sum(x){ var y = function(x){ return sum(x+y) } y.toString = y.valueOf = function(){ return x; } return y; }
Regardons de plus près le curry~
Qu'est-ce que le curry ?
Le currying est un processus de conversion qui transforme une fonction qui accepte plusieurs paramètres en une fonction qui accepte un seul paramètre (remarque : le premier paramètre de la fonction d'origine si d'autres paramètres sont nécessaires, revenez à accepter Une nouvelle fonction qui prend). les paramètres restants et renvoie un résultat.
Je suppose que le curry semble assez simple quand on le dit ainsi. Comment est-il implémenté en JavaScript ?
Supposons que nous voulions écrire une fonction qui accepte 3 paramètres.
var sendMsg = function (from, to, msg) { alert(["Hello " + to + ",", msg, "Sincerely,", "- " + from].join("\n")); };
Maintenant, supposons que nous ayons une fonction curry qui convertit les fonctions JavaScript traditionnelles en fonctions curry :
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"
Curry manuel
Dans l'exemple ci-dessus, nous supposons que nous avons la mystérieuse fonction curry. J'implémenterais une telle fonction, mais pour l'instant, voyons d'abord pourquoi une telle fonction est si nécessaire.
Par exemple, curry manuellement une fonction n'est pas difficile, mais c'est un peu verbeux :
// 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 }; }; };
En JavaScript, même si vous ne spécifiez pas tous les paramètres d'une fonction, la fonction sera quand même appelée. Il s'agit d'une fonctionnalité JavaScript très utile, mais elle crée des problèmes pour le curry.
L'idée est que chaque fonction est une fonction avec un et un seul paramètre. Si vous souhaitez avoir plusieurs paramètres, vous devez définir une série de fonctions imbriquées les unes dans les autres. Détester! Faire cela une ou deux fois, c'est bien, mais lorsque vous devez définir une fonction qui nécessite de nombreux paramètres de cette manière, cela devient assez verbeux et difficile à lire. (Mais ne vous inquiétez pas, je vais vous indiquer le chemin tout de suite)
Certains langages de programmation fonctionnels, comme Haskell et OCaml, intègrent le curry de fonctions dans leur syntaxe. Dans ces langages, par exemple, chaque fonction est une fonction qui prend un argument, et un seul argument. On pourrait penser que cette restriction dépasse les avantages, mais la syntaxe du langage étant ce qu'elle est, cette restriction est presque imperceptible.
Par exemple, en OCaml, vous pouvez définir l'exemple ci-dessus de deux manières :
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 *)
Il est facile de voir à quel point ces deux exemples sont similaires aux deux exemples ci-dessus.
La différence, cependant, est de savoir si la même chose est faite en OCaml. OCaml, il n'existe pas de fonctions avec plusieurs paramètres. Cependant, déclarer plusieurs paramètres sur une seule ligne est un « raccourci » pour imbriquer des fonctions à paramètre unique.
De même, nous nous attendons à ce que l’appel d’une fonction curry soit syntaxiquement similaire à l’appel d’une fonction multi-paramètres dans OCaml. Nous nous attendons à appeler la fonction ci-dessus comme ceci :
example1 foo bar baz example2 foo bar baz
En JavaScript, nous adoptons une approche sensiblement différente :
example1(foo, bar, baz); example2(foo)(bar)(baz);
Dans des langages comme OCaml, le curry est intégré. En JavaScript, bien que le curry soit possible (fonctions d’ordre supérieur), il est syntaxiquement peu pratique. C'est pourquoi nous avons décidé d'écrire une fonction curry pour faire ces choses fastidieuses à notre place et simplifier notre code.
Créer une fonction d'aide au curry
Théoriquement, nous espérons avoir un moyen pratique de convertir d'anciennes fonctions JavaScript (plusieurs paramètres) en fonctions entièrement curry.
Cette idée ne m'est pas propre, d'autres l'ont implémentée, comme la fonction .autoCurry() dans la bibliothèque wu.js (bien que ce qui vous préoccupe soit notre propre implémentation).
Tout d'abord, créons une fonction d'assistance simple .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))); }; }
Prenons un moment pour regarder ce que fait cette fonction. Assez simple. sub_curry accepte une fonction fn comme premier argument, suivie d'un nombre quelconque d'arguments d'entrée. Ce qui est renvoyé est une fonction. Cette fonction renvoie le résultat de l'exécution de fn.apply. La séquence de paramètres combine les paramètres initialement transmis à la fonction, plus les paramètres transmis lorsque fn est appelé.
Voir exemple :
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"]
Évidemment, ce n'est pas ce que nous voulons, mais cela semble un peu curry. Nous allons maintenant définir la fonction curry curry :
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); } }; }
Cette fonction accepte deux paramètres, une fonction et le nombre de paramètres à "curry". Le deuxième paramètre est facultatif. S'il est omis, la propriété Function.prototype.length est utilisée par défaut, juste pour vous indiquer le nombre de paramètres définis par cette fonction.
Au final, nous pouvons démontrer le comportement suivant :
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性能的备注
一些极度关注性能的人可以看看这里,我的意思是,关注下所有这些额外的事情?
通常,是这样,使用柯里化会有一些开销。取决于你正在做的是什么,可能会或不会,以明显的方式影响你。也就是说,我敢说几乎大多数情况,你的代码的拥有性能瓶颈首先来自其他原因,而不是这个。
有关性能,这里有一些事情必须牢记于心: