JavaScript関数のカリー化
カレーとは何ですか?
コンピュータサイエンスにおいて、カリー化(英語:Currying)は、カリー化またはカリー化とも訳され、複数のパラメータを受け入れる関数を、単一のパラメータ(元の関数の最初のパラメータ)を受け入れる関数に変換することです。残りのパラメータを受け入れ、結果を返す新しい関数を返します。この手法は、モーゼス シェーンフィンケルとゴットロブ フレーゲによって発明されましたが、論理学者ハスケル ゲイリーにちなんでクリストファー ストラチーによって命名されました。
直観的に、カリー化は、特定のパラメーターを修正すると、残りのパラメーターを受け入れる関数が得られると主張します。
理論コンピューターサイエンスでは、カリー化は、単一のパラメーターのみを受け入れるラムダ計算など、単純な理論モデルで複数のパラメーターを使用して関数を研究する方法を提供します。
関数カリー化の二重性は Uncurrying です。これは、匿名の単一パラメーター関数を使用して複数パラメーター関数を実装する方法です。
わかりやすい
カリー化の概念は実際には非常に単純で、パラメーターの一部を関数に渡して呼び出し、残りのパラメーターを処理する関数を返すだけです。
3 つの数値の合計を求める関数を実装する必要がある場合:
<span style="font-size: 16px;">function add(x, y, z) {<br> return x + y + z;<br>}<br>console.log(add(1, 2, 3)); // 6<br></span>
<span style="font-size: 16px;">var add = function(x) {<br> return function(y) {<br> return function(z) {<br> return x + y + z;<br> }<br> }<br>}<br><br>var addOne = add(1);<br>var addOneAndTwo = addOne(2);<br>var addOneAndTwoAndThree = addOneAndTwo(3);<br><br>console.log(addOneAndTwoAndThree);<br></span>
ここでは、パラメーターを受け入れて新しい関数を返す add 関数を定義します。 add を呼び出した後、返された関数はクロージャを通じて add の最初のパラメータを記憶します。すべてを一度に呼び出すのは少し面倒ですが、幸いなことに、特別なカリー ヘルパー関数を使用すると、そのような関数の定義と呼び出しを簡単に行うことができます。
ES6 のアロー関数を使用すると、上記の追加を次のように実装できます:
<span style="font-size: 16px;">const add = x => y => z => x + y + z;<br></span>
アロー関数を使用した方がより明確であるようです。
部分的な機能?
この関数を見てみましょう:
<span style="font-size: 16px;">function ajax(url, data, callback) {<br> // ..<br>}<br></span>
複数の異なるインターフェイスに対して HTTP リクエストを開始する必要があるシナリオがあります。これを行うには 2 つの方法があります:
ajax を呼び出す。 () 関数の場合、グローバル URL 定数を渡します。
事前に設定された URL パラメーターを使用して関数参照を作成します。
次に、内部で ajax() リクエストを開始する新しい関数を作成します。さらに、他の 2 つの実際のパラメーターの受信を待機している間に、ajax() の最初の実パラメーターを手動で設定します。 APIアドレスに注意してください。
最初のアプローチでは、次の呼び出しメソッドを生成します:
<span style="font-size: 16px;">function ajaxTest1(data, callback) {<br> ajax('http://www.test.com/test1', data, callback);<br>}<br><br>function ajaxTest2(data, callback) {<br> ajax('http://www.test.com/test2', data, callback);<br>}<br></span>
これら 2 つの類似した関数については、次のパターンを抽出することもできます:
<span style="font-size: 16px;">function beginTest(callback) {<br> ajaxTest1({<br> data: GLOBAL_TEST_1,<br> }, callback);<br>}<br></span>
このパターンを見たことがあると思います: We関数呼び出しサイトで実パラメータを仮パラメータに適用します。ご覧のとおり、最初は引数の一部のみを適用し (具体的には URL パラメーターに適用します)、残りは後で適用します。
上記の概念は部分関数の定義です。部分関数とは、関数のパラメーターの数を減らすプロセスです。ここでのパラメーターの数は、渡されることが期待される仮パラメーターの数を指します。 ajaxTest1() により、元の関数 ajax() のパラメータの数を 3 から 2 に減らしました。
partial() 関数を次のように定義します。
<span style="font-size: 16px;">function partial(fn, ...presetArgs) {<br> return function partiallyApplied(...laterArgs) {<br> return fn(...presetArgs, ...laterArgs);<br> }<br>}<br></span>
partial() 関数は、実際のパラメータを部分的に適用する関数を表す fn パラメータを受け取ります。次に、fn 仮パラメータの後に、presetArgs 配列が後で渡される実際のパラメータを収集し、後で使用できるように保存します。
新しい内部関数を作成して返します (わかりやすくするために、partialApplied(..) という名前を付けます)。この内部関数では、lateArgs 配列がすべての実際のパラメーターを収集します。
アロー関数を使用すると、より簡潔になります:
<span style="font-size: 16px;">var partial =<br> (fn, ...presetArgs) =><br> (...laterArgs) =><br> fn(...presetArgs, ...laterArgs);<br></span>
このモードの部分関数を使用して、前のコードを再構築します:
<span style="font-size: 16px;">function ajax(url, data, callback) {<br> // ..<br>}<br><br>var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');<br>var ajaxTest2 = partial(ajax, 'http://www.test.com/test1');<br></span>
beginTest() 関数についてもう一度考えてみましょう。partial() を使用します。どのようにリファクタリングすればよいでしょうか?
<span style="font-size: 16px;">function ajax(url, data, callback) {<br> // ..<br>}<br><br>// 版本1<br>var beginTest = partial(ajax, 'http://www.test.com/test1', {<br> data: GLOBAL_TEST_1,<br>});<br><br>// 版本2<br>var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');<br>var beginTest = partial(ajaxTest1, {<br> data: GLOBAL_TEST_1,<br>});<br></span>
一度に 1 つずつ渡します
上の例で、バージョン 1 に比べてバージョン 2 の利点が理解できたと思います。はい、カリー化とは、複数のパラメーターを持つ関数の変換は 1 つの関数を実行するプロセスです。一度に。関数が呼び出されるたびに、引数を 1 つだけ受け取り、すべての引数が渡されるまで関数を返します。
The process of converting a function that takes multiple arguments into a function that takes them one at a time.
Each time the function is called it only accepts one argument and returns a function that takes one argument until all arguments are passed.
假设我们已经创建了一个柯里化版本的ajax()函数curriedAjax():
<span style="font-size: 16px;">curriedAjax('http://www.test.com/test1')<br> ({<br> data: GLOBAL_TEST_1,<br> })<br> (function callback(data) {<br> // dosomething<br> });<br></span>
我们将三次调用分别拆解开来,这也许有助于我们理解整个过程:
<span style="font-size: 16px;">var ajaxTest1 = curriedAjax('http://www.test.com/test1');<br><br>var beginTest = ajaxTest1({<br> data: GLOBAL_TEST_1,<br>});<br><br>var ajaxCallback = beginTest(function callback(data) {<br> // dosomething<br>});<br></span>
实现柯里化
那么,我们如何来实现一个自动的柯里化的函数呢?
<span style="font-size: 16px;">var currying = function(fn) {<br> var args = [];<br><br> return function() {<br> if (arguments.length === 0) {<br> return fn.apply(this, args); // 没传参数时,调用这个函数<br> } else {<br> [].push.apply(args, arguments); // 传入了参数,把参数保存下来<br> return arguments.callee; // 返回这个函数的引用<br> }<br> }<br>}<br></span>
调用上述currying()函数:
<span style="font-size: 16px;">var cost = (function() {<br> var money = 0;<br> return function() {<br> for (var i = 0; i < arguments.length; i++) {<br> money += arguments[i];<br> }<br> return money;<br> }<br>})();<br><br>var cost = currying(cost);<br><br>cost(100); // 传入了参数,不真正求值<br>cost(200); // 传入了参数,不真正求值<br>cost(300); // 传入了参数,不真正求值<br><br>console.log(cost()); // 求值并且输出600<br></span>
上述函数是我之前的JavaScript设计模式与开发实践读书笔记之闭包与高阶函数所写的currying版本,现在仔细思考后发现仍旧有一些问题。
我们在使用柯里化时,要注意同时为函数预传的参数的情况。
因此把上述柯里化函数更改如下:
<span style="font-size: 16px;">var currying = function(fn) {<br> var args = Array.prototype.slice.call(arguments, 1);<br><br> return function() {<br> if (arguments.length === 0) {<br> return fn.apply(this, args); // 没传参数时,调用这个函数<br> } else {<br> [].push.apply(args, arguments); // 传入了参数,把参数保存下来<br> return arguments.callee; // 返回这个函数的引用<br> }<br> }<br>}<br></span>
使用实例:
<span style="font-size: 16px;">var cost = (function() {<br> var money = 0;<br> return function() {<br> for (var i = 0; i < arguments.length; i++) {<br> money += arguments[i];<br> }<br> return money;<br> }<br>})();<br><br>var cost = currying(cost, 100);<br>cost(200); // 传入了参数,不真正求值<br>cost(300); // 传入了参数,不真正求值<br><br>console.log(cost()); // 求值并且输出600<br></span>
你可能会觉得每次都要在最后调用一下不带参数的cost()函数比较麻烦,并且在cost()函数都要使用arguments参数不符合你的预期。我们知道函数都有一个length属性,表明函数期望接受的参数个数。因此我们可以充分利用预传参数的这个特点。
借鉴自mqyqingfeng:
<span style="font-size: 16px;">function sub_curry(fn) {<br> var args = [].slice.call(arguments, 1);<br> return function() {<br> return fn.apply(this, args.concat([].slice.call(arguments)));<br> };<br>}<br><br>function curry(fn, length) {<br><br> length = length || fn.length;<br><br> var slice = Array.prototype.slice;<br><br> return function() {<br> if (arguments.length < length) {<br> var combined = [fn].concat(slice.call(arguments));<br> return curry(sub_curry.apply(this, combined), length - arguments.length);<br> } else {<br> return fn.apply(this, arguments);<br> }<br> };<br>}<br></span>
在上述函数中,我们在currying的返回函数中,每次把arguments.length和fn.length作比较,一旦arguments.length达到了fn.length的数量,我们就去调用fn(return fn.apply(this, arguments);)
验证:
<span style="font-size: 16px;">var fn = curry(function(a, b, c) {<br> return [a, b, c];<br>});<br><br>fn("a", "b", "c") // ["a", "b", "c"]<br>fn("a", "b")("c") // ["a", "b", "c"]<br>fn("a")("b")("c") // ["a", "b", "c"]<br>fn("a")("b", "c") // ["a", "b", "c"]<br></span>
bind方法的实现
使用柯里化,能够很方便地借用call()或者apply()实现bind()方法的polyfill。
<span style="font-size: 16px;">Function.prototype.bind = Function.prototype.bind || function(context) {<br> var me = this;<br> var args = Array.prototype.slice.call(arguments, 1);<br> return function() {<br> var innerArgs = Array.prototype.slice.call(arguments);<br> var finalArgs = args.concat(innerArgs);<br> return me.apply(contenxt, finalArgs);<br> }<br>}<br></span>
上述函数有的问题在于不能兼容构造函数。我们通过判断this指向的对象的原型属性,来判断这个函数是否通过new作为构造函数调用,来使得上述bind方法兼容构造函数。
Function.prototype.bind() by MDN如下说到:
绑定函数适用于用new操作符 new 去构造一个由目标函数创建的新的实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。然而, 原先提供的那些参数仍然会被前置到构造函数调用的前面。
这是基于MVC的JavaScript Web富应用开发的bind()方法实现:
<span style="font-size: 16px;">Function.prototype.bind = function(oThis) {<br> if (typeof this !== "function") {<br> throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");<br> }<br><br> var aArgs = Array.prototype.slice.call(arguments, 1),<br> fToBind = this,<br> fNOP = function() {},<br> fBound = function() {<br> return fToBind.apply(<br> this instanceof fNOP && oThis ? this : oThis || window,<br> aArgs.concat(Array.prototype.slice.call(arguments))<br> );<br> };<br><br> fNOP.prototype = this.prototype;<br> fBound.prototype = new fNOP();<br><br> return fBound;<br>};<br></span>
反柯里化(uncurrying)
可能遇到这种情况:拿到一个柯里化后的函数,却想要它柯里化之前的版本,这本质上就是想将类似f(1)(2)(3)的函数变回类似g(1,2,3)的函数。
下面是简单的uncurrying的实现方式:
<span style="font-size: 16px;">function uncurrying(fn) {<br> return function(...args) {<br> var ret = fn;<br><br> for (let i = 0; i < args.length; i++) {<br> ret = ret(args[i]); // 反复调用currying版本的函数<br> }<br><br> return ret; // 返回结果<br> };<br>}<br></span>
注意,不要以为uncurrying后的函数和currying之前的函数一模一样,它们只是行为类似!
<span style="font-size: 16px;">var currying = function(fn) {<br> var args = Array.prototype.slice.call(arguments, 1);<br><br> return function() {<br> if (arguments.length === 0) {<br> return fn.apply(this, args); // 没传参数时,调用这个函数<br> } else {<br> [].push.apply(args, arguments); // 传入了参数,把参数保存下来<br> return arguments.callee; // 返回这个函数的引用<br> }<br> }<br>}<br><br>function uncurrying(fn) {<br> return function(...args) {<br> var ret = fn;<br><br> for (let i = 0; i < args.length; i++) {<br> ret = ret(args[i]); // 反复调用currying版本的函数<br> }<br><br> return ret; // 返回结果<br> };<br>}<br><br>var cost = (function() {<br> var money = 0;<br> return function() {<br> for (var i = 0; i < arguments.length; i++) {<br> money += arguments[i];<br> }<br> return money;<br> }<br>})();<br><br>var curryingCost = currying(cost);<br>var uncurryingCost = uncurrying(curryingCost);<br>console.log(uncurryingCost(100, 200, 300)()); // 600<br></span>
柯里化或偏函数有什么用?
无论是柯里化还是偏应用,我们都能进行部分传值,而传统函数调用则需要预先确定所有实参。如果你在代码某一处只获取了部分实参,然后在另一处确定另一部分实参,这个时候柯里化和偏应用就能派上用场。
另一个最能体现柯里化应用的的是,当函数只有一个形参时,我们能够比较容易地组合它们(单一职责原则(Single responsibility principle))。因此,如果一个函数最终需要三个实参,那么它被柯里化以后会变成需要三次调用,每次调用需要一个实参的函数。当我们组合函数时,这种单元函数的形式会让我们处理起来更简单。
归纳下来,主要为以下常见的三个用途:
延迟计算
参数复用
-
関数の動的生成
以上の内容は、JavaScript関数のカリー化についての説明ですので、皆様のお役に立てれば幸いです。
関連する推奨事項:
以上がJavaScript関数のカリー化の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ホットAIツール

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

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

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

Video Face Swap
完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

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

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

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

ホットトピック











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

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

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

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

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

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

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

C++ の例外処理は、特定のエラー メッセージ、コンテキスト情報を提供し、エラーの種類に基づいてカスタム アクションを実行するカスタム例外クラスを通じて強化できます。 std::Exception から継承した例外クラスを定義して、特定のエラー情報を提供します。カスタム例外をスローするには、throw キーワードを使用します。 try-catch ブロックでdynamic_castを使用して、キャッチされた例外をカスタム例外タイプに変換します。実際の場合、open_file 関数は FileNotFoundException 例外をスローします。例外をキャッチして処理すると、より具体的なエラー メッセージが表示されます。
