2021年1月10日

カリー化

カリー化は関数を扱うための高度なテクニックです。 JavaScriptだけでなく、他の言語でも使用されます。

カリー化とは、関数をf(a, b, c)として呼び出し可能な状態から、f(a)(b)(c)として呼び出し可能な状態に変換することです。

カリー化は関数を呼び出しません。それは単に関数を変換するだけです。

まず、私たちが何を話しているのかをよりよく理解するために、例を見てみましょう。そして実用的な応用例を見てみましょう。

2引数fのカリー化を実行するヘルパー関数curry(f)を作成します。言い換えれば、2引数f(a, b)に対するcurry(f)は、f(a)(b)として実行される関数に変換します。

function curry(f) { // curry(f) does the currying transform
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

// usage
function sum(a, b) {
  return a + b;
}

let curriedSum = curry(sum);

alert( curriedSum(1)(2) ); // 3

ご覧のとおり、実装は簡単です。2つのラッパーがあるだけです。

  • curry(func)の結果はラッパーfunction(a)です。
  • curriedSum(1)のように呼び出されると、引数はレキシカル環境に保存され、新しいラッパーfunction(b)が返されます。
  • 次に、このラッパーは引数として2を使用して呼び出され、元のsumに呼び出しが渡されます。

lodashライブラリの_.curryなどのより高度なカリー化の実装では、関数を通常どおりにも部分的に呼び出すことができるラッパーが返されます。

function sum(a, b) {
  return a + b;
}

let curriedSum = _.curry(sum); // using _.curry from lodash library

alert( curriedSum(1, 2) ); // 3, still callable normally
alert( curriedSum(1)(2) ); // 3, called partially

カリー化? 何のため?

利点を理解するには、価値のある実例が必要です。

たとえば、情報をフォーマットして出力するロギング関数log(date, importance, message)があるとします。実際のプロジェクトでは、このような関数にはネットワークを介してログを送信するなどの多くの便利な機能がありますが、ここではalertを使用します。

function log(date, importance, message) {
  alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}

カリー化しましょう!

log = _.curry(log);

その後、logは正常に機能します。

log(new Date(), "DEBUG", "some debug"); // log(a, b, c)

…しかし、カリー化された形式でも機能します。

log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)

これで、現在のログのための便利な関数を簡単に作成できます。

// logNow will be the partial of log with fixed first argument
let logNow = log(new Date());

// use it
logNow("INFO", "message"); // [HH:mm] INFO message

これで、logNowは最初の引数が固定されたlog、言い換えれば「部分的に適用された関数」または略して「部分」になりました。

さらに進んで、現在のデバッグログのための便利な関数を作成できます。

let debugNow = logNow("DEBUG");

debugNow("message"); // [HH:mm] DEBUG message

つまり、

  1. カリー化後も何も失っていません。logは通常どおりに呼び出し可能です。
  2. 今日のログのような部分関数を簡単に生成できます。

高度なカリー化の実装

詳細を知りたい場合は、上記で使用できる複数引数関数用の「高度な」カリー化の実装を次に示します。

非常に短いです。

function curry(func) {

  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };

}

使用例

function sum(a, b, c) {
  return a + b + c;
}

let curriedSum = curry(sum);

alert( curriedSum(1, 2, 3) ); // 6, still callable normally
alert( curriedSum(1)(2,3) ); // 6, currying of 1st arg
alert( curriedSum(1)(2)(3) ); // 6, full currying

新しいcurryは複雑に見えるかもしれませんが、実際には理解しやすいです。

curry(func)呼び出しの結果は、次のようなラッパーcurriedです。

// func is the function to transform
function curried(...args) {
  if (args.length >= func.length) { // (1)
    return func.apply(this, args);
  } else {
    return function(...args2) { // (2)
      return curried.apply(this, args.concat(args2));
    }
  }
};

実行すると、2つのif実行ブランチがあります。

  1. 渡されたargsの数が、元の関数の定義にある数 (func.length) と同じかそれより多い場合は、func.applyを使用して呼び出しを渡すだけです。
  2. それ以外の場合は、部分を取得します。今はfuncを呼び出しません。代わりに、以前の引数と新しい引数を一緒に提供するcurriedを再適用する別のラッパーが返されます。

次に、それを呼び出すと、再び新しい部分 (引数が足りない場合) または、最終的には結果が得られます。

固定長関数のみ

カリー化では、関数が固定数の引数を持つ必要があります。

f(...args)のような残余パラメータを使用する関数は、この方法ではカリー化できません。

カリー化以上のもの

定義上、カリー化はsum(a, b, c)sum(a)(b)(c)に変換する必要があります。

ただし、JavaScriptのカリー化のほとんどの実装は、説明したように高度であり、多引数バリアントでも関数を呼び出し可能に保ちます。

まとめ

カリー化は、f(a,b,c)f(a)(b)(c)として呼び出し可能にする変換です。 JavaScriptの実装では通常、関数を通常どおりに呼び出し可能に保ち、引数の数が十分でない場合は部分を返します。

カリー化を使用すると、部分を簡単に取得できます。ロギングの例で見たように、3つの引数を持つ普遍的な関数log(date, importance, message)をカリー化すると、1つの引数 (log(date)など) または2つの引数 (log(date, importance)など) で呼び出されたときに部分が得られます。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善するための提案がある場合は、コメントする代わりにGitHub issueを送信するか、プルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • コードを数語挿入するには、<code>タグを使用し、複数行の場合は、<pre>タグで囲み、10行を超える場合はサンドボックス (plnkrjsbincodepen…) を使用してください。