目次
1. 高階関数の落とし穴
2. 函数柯里化(curry)
3. 函数柯里化进一步思考
4. 函数柯里化的递归调用
5. 函数组合(compose)
ホームページ ウェブフロントエンド jsチュートリアル JavaScript 関数のカリー化に関する考え方の詳細な紹介

JavaScript 関数のカリー化に関する考え方の詳細な紹介

Mar 08, 2017 pm 02:20 PM

1. 高階関数の落とし穴

カリー化を学ぶ前に、まず次のコードを見てみましょう:

var f1 = function(x){
    return f(x);
};
f1(x);
ログイン後にコピー

関数 f1 </ code> と <code>f は同等です。var f1 = f; をこのようなレイヤーでラップする必要はありません。 f1f是等效的,我们直接令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;
ログイン後にコピー

如此一来,我们发现那么长一段程序都白写了。

函数既可以当参数,又可以当返回值,是高阶函数的一个重要特性,但是稍不留神就容易踩到坑里。

2. 函数柯里化(curry)

言归正传,什么是函数柯里化?函数柯里化(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虽然是一个新函数,但是f2f是完全等效的,绕了半天,还是绕回来了。

这里有一个很经典的例子:

[&#39;11&#39;, &#39;11&#39;, &#39;11&#39;].map(parseInt) //[ 11, NaN, 3 ]
[&#39;11&#39;, &#39;11&#39;, &#39;11&#39;].map(f1(parseInt)) //[ 11, 11, 11 ]
ログイン後にコピー

由于parseInt接受两个参数,所以直接调用会有进制转换的问题,参考“不愿相离”的文章。

var f2 = f1(parseInt)f2parseInt由原来的接受两个参数变成了只接受一个参数的新函数,从而解决这个进制转换问题。通过我们的f1包裹以后就能够运行出正确的结果了。

有同学觉得这个不算柯里化的应用,我觉得还是算吧,各位同学可以一起来讨论下。

3. 函数柯里化进一步思考

如果说上一节的例子中,我们不是直接运行f(x),而是把函数f当做一个参数,结果会怎样呢?我们来看下面这个例子:

假设f1返回函数gg的作用域指向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:&#39;peng&#39;,1:&#39;chen&#39;,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);
    }
};
ログイン後にコピー
ログイン後にコピー
🎜これは、「JS 関数型プログラミング ガイド」から抜粋したコードです。実際、上記のルールを使用すると、次のコードを得ることができます。 callback は関数 🎜
var match = function(what){
    return function(x){
        return x.match(what);
    }
};
var match2 = match(/q/i);
ログイン後にコピー
ログイン後にコピー
🎜 と同等なので、この関数は次のように簡略化できます: 🎜
var filterQs =  filter(match2);
var xs = [&#39;q&#39;,&#39;test1&#39;,&#39;test2&#39;];
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
ログイン後にコピー
ログイン後にコピー
🎜 このようにして、このような長いプログラムが次のように記述されていることがわかります。うぬぼれが強い。 🎜🎜関数はパラメータとしても戻り値としても使用できます。これは高階関数の重要な機能ですが、注意しないと落とし穴に陥りやすいです。 🎜🎜2. ファンクションカレー🎜🎜本題に戻りますが、ファンクションカレーとは何でしょうか?関数カリーは、パラメータの一部だけを渡して関数を呼び出し、残りのパラメータを処理する関数を返すようにします。ややこしいように思えますが、実際には関数の変数を分割して呼び出します。 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(&#39; &#39;).map(compose(toUpperCase, head)).join(&#39;. &#39;);
};

// pointfree
var initials = compose(join(&#39;. &#39;), map(compose(toUpperCase, head)), split(&#39; &#39;));

initials("hunter stockton thompson");
// &#39;H. S. T&#39;
ログイン後にコピー
ログイン後にコピー
🎜実際、この例は不適切であると気がつくかもしれません。f1 は新しい関数ですが、f2f は完全に新しい関数です。かなり遠回りしたのに、また戻ってきました。 🎜🎜これは非常に古典的な例です: 🎜rrreee🎜 parseInt は 2 つのパラメータを受け入れるため、直接呼び出すと 16 進数変換の問題が発生します。記事「分割したくない」を参照してください。 🎜🎜var f2 = f1(parseInt)f2 は、parseInt を 2 つのパラメーターを受け入れるものから、解決するパラメーター関数を 1 つだけ受け入れる新しいものに変更します。この16進数変換の問題。 f1 パッケージを渡すと、正しい結果を実行できるようになります。 🎜
🎜これはカレーのような申請ではないと考える生徒もいます。生徒全員で話し合うべきだと思います。 🎜
🎜3. 関数のカリー化についてのさらなる考察🎜🎜前のセクションの例では、f(x) を直接実行する代わりに、関数 f< を置きます。 /code> をパラメータとして使用すると、結果はどうなりますか?次の例を見てみましょう: 🎜🎜 <code>f1 が関数 g を返し、g のスコープが xs を指しているとします。 >、関数 fg へのパラメータとして機能します。最後に、次の形式で書くことができます: 🎜rrreee🎜 実際、f1 を使用して g.call(xxx) を置き換えることは、デカリングと呼ばれます。例: 🎜rrreee
🎜アンチカリングとは、元の固定パラメータまたはこのコンテキストをパラメータとして将来に延期することです。
慣れれば機能を大幅に簡素化できます。 🎜
🎜 アンチカリーはさておき、f1 をカリーにしたい場合はどうすればよいでしょうか? 🎜🎜クロージャを使用すると、次の形式で書くことができます: 🎜rrreee🎜 ff1 に渡すと、新しい 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函数传入的回调函数是matchmatch的主要功能是判断每个字符串是否匹配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 = [&#39;q&#39;,&#39;test1&#39;,&#39;test2&#39;];
filterQs(xs);
ログイン後にコピー
ログイン後にコピー

从这个示例中我们也可以体会到函数柯里化的强大。所以,柯里化还有一个重要的功能:封装不同功能的函数,利用已有的函数组成新的函数。

4. 函数柯里化的递归调用

函数柯里化还有一种有趣的形式,就是函数可以在闭包中调用自己,类似于函数递归调用。如下所示:

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都会生成一个新的闭包。

5. 函数组合(compose)

函数组合是在柯里化基础上完成的:

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(&#39; &#39;).map(compose(toUpperCase, head)).join(&#39;. &#39;);
};

// pointfree
var initials = compose(join(&#39;. &#39;), map(compose(toUpperCase, head)), split(&#39; &#39;));

initials("hunter stockton thompson");
// &#39;H. S. T&#39;
ログイン後にコピー
ログイン後にコピー

以上がJavaScript 関数のカリー化に関する考え方の詳細な紹介の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

golang 関数で新しい関数を動的に作成するためのヒント golang 関数で新しい関数を動的に作成するためのヒント Apr 25, 2024 pm 02:39 PM

Go 言語は、クロージャとリフレクションという 2 つの動的関数作成テクノロジを提供します。クロージャを使用すると、クロージャ スコープ内の変数にアクセスでき、リフレクションでは FuncOf 関数を使用して新しい関数を作成できます。これらのテクノロジーは、HTTP ルーターのカスタマイズ、高度にカスタマイズ可能なシステムの実装、プラグイン可能なコンポーネントの構築に役立ちます。

C++ 関数の命名におけるパラメーターの順序に関する考慮事項 C++ 関数の命名におけるパラメーターの順序に関する考慮事項 Apr 24, 2024 pm 04:21 PM

C++ 関数の名前付けでは、読みやすさを向上させ、エラーを減らし、リファクタリングを容易にするために、パラメーターの順序を考慮することが重要です。一般的なパラメータの順序規則には、アクション-オブジェクト、オブジェクト-アクション、意味論的な意味、および標準ライブラリへの準拠が含まれます。最適な順序は、関数の目的、パラメーターの種類、潜在的な混乱、および言語規約によって異なります。

Java で効率的で保守しやすい関数を記述するにはどうすればよいでしょうか? Java で効率的で保守しやすい関数を記述するにはどうすればよいでしょうか? Apr 24, 2024 am 11:33 AM

効率的で保守しやすい Java 関数を作成するための鍵は、シンプルに保つことです。意味のある名前を付けてください。特殊な状況に対処します。適切な可視性を使用してください。

Excel関数の公式の完全なコレクション Excel関数の公式の完全なコレクション May 07, 2024 pm 12:04 PM

1. SUM 関数は、列またはセルのグループ内の数値を合計するために使用されます (例: =SUM(A1:J10))。 2. AVERAGE 関数は、列またはセルのグループ内の数値の平均を計算するために使用されます (例: =AVERAGE(A1:A10))。 3. COUNT 関数。列またはセルのグループ内の数値またはテキストの数をカウントするために使用されます。例: =COUNT(A1:A10)。 4. IF 関数。指定された条件に基づいて論理的な判断を行い、結果を返すために使用されます。対応する結果。

C++関数のデフォルトパラメータと可変パラメータの長所と短所の比較 C++関数のデフォルトパラメータと可変パラメータの長所と短所の比較 Apr 21, 2024 am 10:21 AM

C++ 関数のデフォルト パラメーターの利点には、呼び出しの簡素化、可読性の向上、エラーの回避などがあります。欠点は、柔軟性が限られていることと、名前の制限があることです。可変引数パラメーターの利点には、無制限の柔軟性と動的バインディングが含まれます。欠点としては、複雑さの増大、暗黙的な型変換、デバッグの難しさなどが挙げられます。

参照型を返す C++ 関数の利点は何ですか? 参照型を返す C++ 関数の利点は何ですか? Apr 20, 2024 pm 09:12 PM

C++ で参照型を返す関数の利点は次のとおりです。 パフォーマンスの向上: 参照による受け渡しによりオブジェクトのコピーが回避され、メモリと時間が節約されます。直接変更: 呼び出し元は、返された参照オブジェクトを再割り当てせずに直接変更できます。コードの簡素化: 参照渡しによりコードが簡素化され、追加の代入操作は必要ありません。

カスタム PHP 関数と定義済み関数の違いは何ですか? カスタム PHP 関数と定義済み関数の違いは何ですか? Apr 22, 2024 pm 02:21 PM

カスタム PHP 関数と定義済み関数の違いは次のとおりです。 スコープ: カスタム関数はその定義のスコープに限定されますが、事前定義関数はスクリプト全体からアクセスできます。定義方法: カスタム関数は function キーワードを使用して定義されますが、事前定義関数は PHP カーネルによって定義されます。パラメータの受け渡し: カスタム関数はパラメータを受け取りますが、事前定義された関数はパラメータを必要としない場合があります。拡張性: カスタム関数は必要に応じて作成できますが、事前定義された関数は組み込みで変更できません。

C++ 関数での参照パラメータとポインタ パラメータの高度な使用法 C++ 関数での参照パラメータとポインタ パラメータの高度な使用法 Apr 21, 2024 am 09:39 AM

C++ 関数の参照パラメーター (基本的には変数のエイリアス、参照を変更すると元の変数が変更されます) とポインター パラメーター (元の変数のメモリ アドレスを保存し、ポインターを逆参照して変数を変更します) は、変数を渡したり変更したりするときに使用方法が異なります。参照パラメーターは、コンストラクターまたは代入演算子に渡されるときのコピーのオーバーヘッドを避けるために、元の変数 (特に大きな構造体) を変更するためによく使用されます。ポインター パラメーターは、メモリの場所を柔軟に指定したり、動的データ構造を実装したり、オプションのパラメーターを表すために null ポインターを渡したりするために使用されます。

See all articles