JavaScript関数のカリー化

小云云
小云云オリジナル
2017-12-06 16:11:271454ブラウズ

カレーとは何ですか?

コンピュータサイエンスにおいて、カリー化(英語: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関数のカリー化についての説明ですので、皆様のお役に立てれば幸いです。

関連する推奨事項:

JSカリー化の詳細な例

JSのアンチカリー化

JavaScript関数カリー化の詳細な説明

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

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