2023年7月30日

カリー化

カリー化は、関数を扱う際の上級テクニックです。これは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)によって変形が施されます
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}
// 使用法
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への呼び出しを返します。

例えばloadashライブラリの_.curryのような、もっと高度なカリー化の実装では、関数を通常通り呼んだり部分的に呼んだりすることが認められるようなラッパが返されます。

function sum(a, b) {
  return a + b;
}
let curriedSum = _.curry(sum); // loadashライブラリの _.curry を使用します
alert( curriedSum(1, 2) ); // 3, 通常通り呼び出します
alert( curriedSum(1)(2) ); // 3, 部分的に呼び出します

カリー化?なんのために?

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

例えば、情報を整形して出力する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はlogの部分関数で、第1引数が固定されています
let logNow = log(new Date());

// それを使用します
logNow("INFO", "message"); // [HH:mm] INFO message

これで、logNowは第1引数が固定された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, 普通に呼び出すことも可能です
alert( curriedSum(1)(2,3) ); // 6, 最初の引数をカリー化しています
alert( curriedSum(1)(2)(3) ); // 6, 完全なカリー化です

新しいcurryは複雑なように見えますが、実際は簡単に理解できます。

curry(func)呼び出しの結果は、ラップされた以下のようなcurried関数です。

// funcは変形対象の関数です
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に再び適用します。

それから繰り返しになりますが、curriedを呼び出せば、新しい部分適用(引数の数が十分ではない場合)か、最終的には結果が得られます。

固定長の関数のみ

カリー化では、関数の引数の数は固定されている必要があります。

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

カリー化よりもさらに

定義上、カリー化は sum(a, b, c) を sum(a)(b)© に変換するべきです。

しかし、JavaScriptでのカリー化のほとんどの実装は説明されているように高度であり、複数引数のバリアントでも関数が呼び出し可能となっています。

サマリ

カリー化f(a,b,c)f(a)(b)(c)として呼び出し可能に変換します。JavaScriptの実装は、通常の形で呼び出し可能な関数を維持し、かつ引数が不足している場合には部分適用を返します。

簡単な部分適用がほしいときにカリー化は素晴らしいです。ロギングの例で見てきたように、カリー化後の3引数を取る汎用的な関数log(date, importance, message)は、(log(date)のような)1つの引数または(log(date, importance)のような)2つの引数で呼び出された時には部分適用を返します。

タスク

重要性: 5

このタスクは Ask losing this 少しより複雑なバリアントです。

user オブジェクトが修正されました。今、2つの関数 loginOk/loginFail の代わりに、単一の関数 user.login(true/false) があります。

下のコードでは、何を渡すと ok として user.login(true) を、fail として user.login(fail) を呼ぶでしょうか?

function askPassword(ok, fail) {
  let password = prompt("Password?", '');
  if (password == "rockstar") ok();
  else fail();
}

let user = {
  name: 'John',

  login(result) {
    alert( this.name + (result ? ' logged in' : ' failed to log in') );
  }
};

askPassword(?, ?); // ?

変更はハイライトされた箇所の修正だけにしてください。

  1. ラッパー関数、簡単化のためにアローを使う:

    askPassword(() => user.login(true), () => user.login(false));

    これで外部変数から user を取得し、通常の方法で実行します。

  2. もしくは、user をコンテキストとして使い、正しい1つ目の引数を持つ user.login からの部分関数を作ります:

    askPassword(user.login.bind(user, true), user.login.bind(user, false));
チュートリアルマップ