Heim > Web-Frontend > js-Tutorial > Eine eingehende Analyse des Currying von Funktionen in JavaScript_Javascript-Kenntnissen

Eine eingehende Analyse des Currying von Funktionen in JavaScript_Javascript-Kenntnissen

WBOY
Freigeben: 2016-05-16 15:10:05
Original
1688 Leute haben es durchsucht

Einführung
Schauen wir uns zunächst eine kleine Frage an:
Jemand hat eine Frage in der Gruppe gepostet:
var s = sum(1)(2)(3) ....... Schließlich wird die Warnung(en) als 6
ausgegeben var s = sum(1)(2)(3)(4) ....... Schließlich wird die Warnung(en) als 10
ausgegeben Fragen Sie, wie Sie die Summe implementieren?
Als ich den Titel zum ersten Mal sah, war meine erste Reaktion, dass sum eine Funktion zurückgibt, die jedoch nicht endgültig implementiert wurde. Ich hatte ein ähnliches Prinzip in meinem Kopf gesehen, konnte mich aber nicht genau daran erinnern.

Später sagte ein Kollege, dass man das Currying nennt,
Die Implementierungsmethode ist cleverer:

function sum(x){ 
 var y = function(x){ 
  return sum(x+y) 
 } 
 y.toString = y.valueOf = function(){ 
  return x; 
 } 
 return y; 
} 
Nach dem Login kopieren

Schauen wir uns das Curry genauer an~

Was ist Curry?

Currying ist ein Konvertierungsprozess, der eine Funktion, die mehrere Parameter akzeptiert, in eine Funktion umwandelt, die einen einzelnen Parameter akzeptiert (Hinweis: Wenn andere Parameter erforderlich sind, kehren Sie zurück, um eine neue Funktion zu akzeptieren). die restlichen Parameter und gibt ein Ergebnis zurück.

Ich denke, Curry klingt ziemlich einfach, wenn wir es so ausdrücken. Wie wird es in JavaScript implementiert?
Angenommen, wir möchten eine Funktion schreiben, die drei Parameter akzeptiert.

var sendMsg = function (from, to, msg) {
 alert(["Hello " + to + ",", msg, "Sincerely,", "- " + from].join("\n"));
};
Nach dem Login kopieren

Angenommen, wir haben eine Curry-Funktion, die herkömmliche JavaScript-Funktionen in Curry-Funktionen umwandelt:

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"

Nach dem Login kopieren

Manuelles Curryieren

Im obigen Beispiel gehen wir davon aus, dass wir über die mysteriöse Curry-Funktion verfügen. Ich würde eine solche Funktion implementieren, aber zunächst wollen wir sehen, warum eine solche Funktion so notwendig ist.
Das manuelle Currying einer Funktion ist beispielsweise nicht schwierig, aber etwas ausführlich:

// 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
  };
 };
};
Nach dem Login kopieren

In JavaScript wird die Funktion trotzdem aufgerufen, auch wenn Sie nicht alle Parameter einer Funktion angeben. Dies ist eine sehr nützliche JavaScript-Funktion, die jedoch Probleme beim Currying verursacht.

Die Idee ist, dass jede Funktion eine Funktion mit einem und nur einem Parameter ist. Wenn Sie mehrere Parameter haben möchten, müssen Sie eine Reihe ineinander verschachtelter Funktionen definieren. Hassen! Es ist in Ordnung, dies ein- oder zweimal zu tun, aber wenn Sie auf diese Weise eine Funktion definieren müssen, die viele Parameter erfordert, wird es ziemlich ausführlich und schwer zu lesen. (Aber keine Sorge, ich verrate dir gleich einen Weg)

Einige funktionale Programmiersprachen wie Haskell und OCaml verfügen über ein integriertes Funktions-Currying in ihrer Syntax. In diesen Sprachen ist beispielsweise jede Funktion eine Funktion, die ein Argument akzeptiert, und zwar nur ein Argument. Man könnte meinen, dass diese Einschränkung die Vorteile überwiegt, aber angesichts der Syntax der Sprache ist diese Einschränkung kaum wahrnehmbar.

In OCaml können Sie das obige Beispiel beispielsweise auf zwei Arten definieren:

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 *)
Nach dem Login kopieren

Es ist leicht zu erkennen, dass diese beiden Beispiele den beiden obigen Beispielen ähneln.

Der Unterschied besteht jedoch darin, ob das Gleiche in OCaml geschieht. OCaml, es gibt keine Funktionen mit mehreren Parametern. Allerdings ist die Deklaration mehrerer Parameter in einer Zeile eine „Abkürzung“ zur Verschachtelung von Funktionen mit nur einem Parameter.

In ähnlicher Weise erwarten wir, dass der Aufruf einer Curry-Funktion syntaktisch dem Aufruf einer Funktion mit mehreren Parametern in OCaml ähnelt. Wir gehen davon aus, dass wir die obige Funktion wie folgt aufrufen:

example1 foo bar baz
example2 foo bar baz
Nach dem Login kopieren

In JavaScript verfolgen wir einen deutlich anderen Ansatz:

example1(foo, bar, baz);
example2(foo)(bar)(baz);
Nach dem Login kopieren

In Sprachen wie OCaml ist Currying integriert. In JavaScript ist Currying zwar möglich (Funktionen höherer Ordnung), aber syntaktisch unpraktisch. Aus diesem Grund haben wir uns entschieden, eine Curry-Funktion zu schreiben, die diese mühsamen Dinge für uns erledigt und unseren Code einfacher macht.

Erstellen Sie eine Curry-Hilfsfunktion

Theoretisch hoffen wir, eine bequeme Möglichkeit zu haben, einfache alte JavaScript-Funktionen (mehrere Parameter) in vollständig Curry-Funktionen umzuwandeln.

Diese Idee ist nicht einzigartig für mich, andere haben sie implementiert, beispielsweise die Funktion .autoCurry() in der wu.js-Bibliothek (obwohl Sie sich Sorgen um unsere eigene Implementierung machen).

Erstellen wir zunächst eine einfache Hilfsfunktion .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)));
 };
}
Nach dem Login kopieren

Nehmen wir uns einen Moment Zeit, um uns anzusehen, was diese Funktion bewirkt. Ganz einfach. sub_curry akzeptiert eine Funktion fn als erstes Argument, gefolgt von einer beliebigen Anzahl von Eingabeargumenten. Was zurückgegeben wird, ist eine Funktion. Diese Funktion gibt das Ausführungsergebnis von fn.apply zurück. Die Parametersequenz kombiniert die anfänglich an die Funktion übergebenen Parameter sowie die Parameter, die beim Aufruf von fn übergeben wurden.

Siehe Beispiel:

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"]
Nach dem Login kopieren

Natürlich ist das nicht das, was wir wollen, aber es scheint ein bisschen kurios zu sein. Jetzt definieren wir die Curry-Funktion 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 
    &#63; 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);
  }
 };
}
Nach dem Login kopieren

Diese Funktion akzeptiert zwei Parameter, eine Funktion und die Anzahl der Parameter, die „curry“ werden sollen. Der zweite Parameter ist optional. Wenn er weggelassen wird, wird standardmäßig die Eigenschaft Function.prototype.length verwendet, um Ihnen mitzuteilen, wie viele Parameter diese Funktion definiert.

Letztendlich können wir folgendes Verhalten nachweisen:

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"]
Nach dem Login kopieren

我知道你在想什么…

等等…什么?!

难道你疯了?应该是这样!我们现在能够在JavaScript中编写柯里化函数,表现就如同OCaml或者Haskell中的那些函数。甚至,如果我想要一次传递多个参数,我可以向我从前做的那样,用逗号分隔下参数就可以了。不需要参数间那些丑陋的括号,即使是它是柯里化后的。

这个相当有用,我会立即马上谈论这个,可是首先我要让这个Curry函数前进一小步。

柯里化和“洞”(“holes”)

尽管柯里化函数已经很牛了,但是它也让你必须花费点小心思在你所定义函数的参数顺序上。终究,柯里化的背后思路就是创建函数,更具体的功能,分离其他更多的通用功能,通过分步应用它们。

当然这个只能工作在当最左参数就是你想要分步应用的参数!

为了解决这个,在一些函数式编程语言中,会定义一个特殊的“占位变量”。通常会指定下划线来干这事,如过作为一个函数的参数被传入,就表明这个是可以“跳过的”。是尚待指定的。

这是非常有用的,当你想要分步应用(partially apply)一个特定函数,但是你想要分布应用(partially apply)的参数并不是最左参数。

举个例子,我们有这样的一个函数:

var sendAjax = function (url, data, options) { 
/* ... */
 }
Nach dem Login kopieren

也许我们想要定义一个新的函数,我们部分提供SendAjax函数特定的Options,但是允许url和data可以被指定。

当然了,我们能够相当简单的这样定义函数:

var sendPost = function (url, data) {
 return sendAjax(url, data, { type: "POST", contentType: "application/json" });
};
Nach dem Login kopieren

或者,使用使用约定的下划线方式,就像下面这样:

var sendPost = sendAjax( _ , _ , { type: "POST", contentType: "application/json" });
Nach dem Login kopieren

注意两个参数以下划线的方式传入。显然,JavaScript并不具备这样的原生支持,于是我们怎样才能这样做呢?

回过头让我们把curry函数变得智能一点…

首先我们把我们的“占位符”定义成一个全局变量。

var _ = {};
Nach dem Login kopieren

我们把它定义成对象字面量{},便于我们可以通过===操作符来判等。

不管你喜不喜欢,为了简单一点我们就使用_来做“占位符”。现在我们就可以定义新的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);
  }
 }
}
Nach dem Login kopieren

实际代码还是有着巨大不同的。 我们这里做了一些关于这些“洞”(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]
Nach dem Login kopieren

疯狂吧?!

我为什么要关心?柯里化能够怎么帮助我?

你可能会停在这儿思考…

这看起来挺酷而且…但是这真的能帮助我编写更好的代码?

这里有很多原因关于为什么函数柯里化是有用的。

函数柯里化允许和鼓励你分隔复杂功能变成更小更容易分析的部分。这些小的逻辑单元显然是更容易理解和测试的,然后你的应用就会变成干净而整洁的组合,由一些小单元组成的组合。

为了给一个简单的例子,让我们分别使用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);
};

Nach dem Login kopieren

函数化方式

//+ String -> [String]
var processLine = compose( map(trim) , split(",") );
 
//+ String -> [[String]]
var parseCSV = compose( map(processLine) , split("\n") );
Nach dem Login kopieren

所有这些例子功能上是等价的。我有意的尽可能的简单的编写这些。

想要达到某种效果是很难的,但是主观上这些例子,我真的认为最后一个例子,函数式方式的,体现了函数式编程背后的威力。

关于curry性能的备注

一些极度关注性能的人可以看看这里,我的意思是,关注下所有这些额外的事情?

通常,是这样,使用柯里化会有一些开销。取决于你正在做的是什么,可能会或不会,以明显的方式影响你。也就是说,我敢说几乎大多数情况,你的代码的拥有性能瓶颈首先来自其他原因,而不是这个。

有关性能,这里有一些事情必须牢记于心:

  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上
  • 在大多是web应用中,“瓶颈”会发生在操控DOM上。这是非常不可能的,你在所有方面关注性能。显然,用不用上面的代码自行考虑。

Verwandte Etiketten:
Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage