JavaScript における関数のカリー化の詳細な分析_javascript スキル

WBOY
リリース: 2016-05-16 15:10:05
オリジナル
1657 人が閲覧しました

はじめに
最初に小さな質問を見てみましょう:
誰かがグループに質問を投稿しました:
var s = sum(1)(2)(3) ..... 最終的にアラートは 6 つ出てきます
var s = sum(1)(2)(3)(4) ....... 最終的にアラートは 10 個になりました
sum を実装する方法を尋ねますか?
最初にタイトルを見たとき、私の最初の反応は、sum は関数を返すが、最終的には実装されていないということでした。同様の原理を頭の中で見たことがありましたが、はっきりとは思い出せませんでした。

後で同僚は、これをカリー化と呼ぶと言いました、
実装方法はより賢明です:

function sum(x){ 
 var y = function(x){ 
  return sum(x+y) 
 } 
 y.toString = y.valueOf = function(){ 
  return x; 
 } 
 return y; 
} 
ログイン後にコピー

カレー作りを詳しく見てみましょう~

カレーとは何ですか?

カリー化とは、複数のパラメーターを受け入れる関数を、単一のパラメーター (注: 元の関数の最初のパラメーター) を受け入れる関数に変換する変換プロセスです。他のパラメーターが必要な場合は、受け入れに戻ります。残りのパラメータを処理し、結果を返します。

そう言うと、カリー化は非常に簡単に聞こえると思います。 JavaScript ではどのように実装されますか?
3 つのパラメーターを受け入れる関数を作成するとします。

var sendMsg = function (from, to, msg) {
 alert(["Hello " + to + ",", msg, "Sincerely,", "- " + from].join("\n"));
};
ログイン後にコピー

ここで、従来の JavaScript 関数をカリー化された関数に変換するカリー化された関数があるとします。

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"

ログイン後にコピー

手動カリー化

上記の例では、謎のカレー関数があると仮定しています。このような機能を実装することになりますが、ここではまず、なぜそのような機能が必要なのかを見てみましょう。
たとえば、関数を手動でカリー化することは難しくありませんが、少し冗長になります:

// 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
  };
 };
};
ログイン後にコピー

JavaScript では、関数のすべてのパラメーターを指定しなくても、関数は呼び出されます。これは非常に便利な JavaScript 機能ですが、カリー化には問題が生じます。

すべての関数はパラメーターを 1 つだけ持つ関数であるという考え方です。複数のパラメーターが必要な場合は、相互にネストされた一連の関数を定義する必要があります。嫌い!これを 1 〜 2 回行うのは問題ありませんが、この方法で多くのパラメータを必要とする関数を定義する必要がある場合、非常に冗長になり、読みにくくなります。 (でも心配しないでください、すぐに方法を教えます)

Haskell や OCaml などの一部の関数型プログラミング言語には、構文に関数カリー化が組み込まれています。たとえば、これらの言語では、すべての関数は引数を 1 つだけ取る関数です。この制限が利点を上回ると思われるかもしれませんが、言語の構文が実際のものであるため、この制限はほとんど認識されません。

たとえば、OCaml では、上記の例を 2 つの方法で定義できます。

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 *)
ログイン後にコピー

これら 2 つの例が上の 2 つの例とどのように似ているかは簡単にわかります。

ただし、違いは、同じことが OCaml で行われるかどうかです。 OCaml には、複数のパラメーターを持つ関数はありません。ただし、1 行で複数のパラメーターを宣言することは、単一パラメーター関数をネストするための「近道」です。

同様に、カリー化された関数の呼び出しは、OCaml でのマルチパラメーター関数の呼び出しと構文的に似ていると予想されます。上記の関数を次のように呼び出すことを想定しています:

example1 foo bar baz
example2 foo bar baz
ログイン後にコピー

JavaScript では、大きく異なるアプローチを採用します。

example1(foo, bar, baz);
example2(foo)(bar)(baz);
ログイン後にコピー

OCaml のような言語では、カリー化が組み込まれています。 JavaScript では、カリー化 (高階関数) が可能ですが、構文的に不便です。このため、これらの面倒な作業を実行し、コードを簡素化するためにカリー化された関数を作成することにしました。

カレーヘルパー関数を作成します

理論的には、単純な古い JavaScript 関数 (複数のパラメーター) を完全にカリー化された関数に変換する便利な方法があればと考えています。

このアイデアは私に特有のものではなく、wu.js ライブラリの .autoCurry() 関数など、他の人も実装しています (ただし、あなたが懸念しているのは私たち自身の実装です)。

まず、簡単なヘルパー関数 .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)));
 };
}
ログイン後にコピー

この関数が何をするのか見てみましょう。とてもシンプルです。 sub_curry は関数 fn を最初の引数として受け入れ、その後に任意の数の入力引数を受け入れます。返されるのは関数であり、この関数は fn.apply の実行結果を返します。パラメータ シーケンスは、最初に関数に渡されたパラメータと、fn が呼び出されたときに渡されたパラメータを組み合わせたものです。

例を参照:

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"]
ログイン後にコピー

明らかに、これは私たちが望んでいることではありませんが、少し面倒に思えます。次に、カリー化関数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);
  }
 };
}
ログイン後にコピー

この関数は、関数と「カリー化」されるパラメーターの数という 2 つのパラメーターを受け入れます。 2 番目のパラメーターはオプションです。省略した場合、この関数が定義するパラメーターの数を示すために、Function.prototype.length プロパティがデフォルトで使用されます。

最終的には、次の動作を実証できます:

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性能的备注

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

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

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

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

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!