2019年9月20日

カリー化と部分適用

今まで、this をバインドすることについて話していました。 さあ、もう一歩を進めましょう。

私たちは、this だけでなく、引数もバインドすることができます。それはめったにされませんが、便利なときがあります。

bind の完全な構文です:

let bound = func.bind(context, arg1, arg2, ...);

これは、コンテキスト( this として)と、関数の開始引数をバインドすることができます。

例えば、乗算関数 mul(a, b) を考えます:

function mul(a, b) {
  return a * b;
}

これをベースとした関数 double を作るために、bind を使ってみましょう。:

let double = mul.bind(null, 2);

alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10

mul.bind(null, 2) を呼び出すと、mul を呼び出す新しい関数 double が作成されます。それは、null がコンテキストとして、2 が最初の引数として固定されます。さらに、その他の引数は “そのまま” 渡されます。

これは 関数への部分的な適用(partial function application)と呼ばれます – 既存の関数の一部のパラメータを変更することで新しい関数を作ります。

ここで、例では実際には this を使っていないことに注意してください。しかし bind はそれを必要とするため、null のような何かを指定する必要があります。

下のコードの関数 triple は値を3倍にします:

let triple = mul.bind(null, 3);

alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15

なぜ、通常部分的な関数を作るのでしょうか?

ここでの我々のメリットは、分かりやすい名前(double, triple)で独立した関数を作れたことです。私たちはそれを使うことができ、毎回最初の引数を書く必要がありません。なぜなら、bind で固定されているからです。

別のケースでは、非常に汎用的な関数を持っており、利便性のために汎用性を減らしたい時に部分適用は役立ちます。

例えば、関数 send(from, to, text) を考えます。次に、user オブジェクトの内側で、その部分的なバリアントを使いたいかもしれません。: 現在のユーザから送信を行う sendTo(to, text) 関数など。

コンテキストなしの部分適用

仮に、いくつかの引数を修正したいが、this をバインドしない場合はどうなるでしょう?

ネイティブの bind ではそれは許可されていません。単にコンテキストを省略し引数にジャンプすることは出来ません。

ただ、幸いにも引数だけをバインドする partial 関数は簡単に実装することが出来ます。

このようになります:

function partial(func, ...argsBound) {
  return function(...args) { // (*)
    return func.call(this, ...argsBound, ...args);
  }
}

// 使い方:
let user = {
  firstName: "John",
  say(time, phrase) {
    alert(`[${time}] ${this.firstName}: ${phrase}!`);
  }
};

// 最初の引数を固定して何かを表示する部分的なメソッドを追加する
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());

user.sayNow("Hello");
// このようになります:
// [10:00] Hello, John!

partial(func[, arg1, arg2...]) の呼び出しの結果は次のように func を呼び出すラッパー (*) です。:

  • this はそれが取得したものと同じです(user.sayNow の場合 user です)。
  • 次に、...argsBound を与えます – partial 呼び出しからの引数("10:00")です。
  • 次に、...args です – ラッパーへ与えられた引数("Hello") です。

なのでスプレッド演算子を使えば簡単ですね。

また、lodashライブラリの _.partial 実装も用意されています。

カリー化(Currying)

“カリー化” と呼ばれる別のものと、上で言及された関数の部分適用を混同する人もいます。それらはここで言及しておくべき関数を扱う別の興味深いテクニックです。

Curryingf(a, b, c) と呼び出し可能なものを f(a)(b)(c) として呼び出しできるように変換します。

2変数関数に対するカリー化を行う関数 curry を作ってみましょう。つまり、f(a, b)f(a)(b) に変換します。:

function curry(func) {
  return function(a) {
    return function(b) {
      return func(a, b);
    };
  };
}

// 使い方
function sum(a, b) {
  return a + b;
}

let carriedSum = curry(sum);

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

上でわかるように、実装はラッパーの連続です。

  • curry(func) の結果はラッパー function(a) です。
  • sum(1) のように呼ばれるとき、引数はレキシカル環境に保存され、新しいラッパー function(b) が返却されます。
  • そして、最終的に sum(1)(2)2function(b) を呼び、それは元の複数引数を取る sum を呼びます。

lodash の _.curry のようなカリー化のより高度な実装は、より洗練された処理を行います。それらの関数は全ての引数が提供された場合には関数が正常に呼び出されるようなラッパーを返し、そうでない場合 には、部分適用を返します。

function curry(f) {
  return function(..args) {
    // もし args.length == f.length の場合(f と同じ引数がある場合),
    //   呼び出しを f へ渡す
    // それ以外は argsを最初の引数として固定する部分関数を返す
  };
}

カリー化? 何のために?

高度なカリー化を使用すると、簡単に関数を通常呼び出し可能にしつつ、部分適用をすることができます。このメリットを理解するために、価値のある実例を見る必要があります。

例えば、情報を整形して出力するロギング関数 log(date, importance, message) を持っているとします。実際のプロジェクトでは、このような関数には、ネットワーク経由での送信やフィルタリングなど、他にも多くの便利な機能があります。

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

では、カリー化してみましょう!

log = _.curry(log);

この処理の後でも log は通常の方法で動きます:

log(new Date(), "DEBUG", "some debug");

…しかしカリー化された形式でも呼び出すことができます:

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

今日のログのための便利な関数を取得してみましょう:

// todayLog は最初の引数が固定された log の一部になります
let todayLog = log(new Date());

// 使ってみます
todayLog("INFO", "message"); // [HH:mm] INFO message

また、これで今日のデバッグメッセージのための便利関数ができます:

let todayDebug = todayLog("DEBUG");

todayDebug("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

// curried(1)で部分を取得し、他の2つの引数で呼び出す
alert( curriedSum(1)(2,3) ); // 6

// 完全にカリー化された呼び出し
alert( curriedSum(1)(2)(3) ); // 6

新しい 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 pass(...args2) { // (2)
      return curried.apply(this, args.concat(args2));
    }
  }
};

これを実行すると、2つの分岐があります。:

  1. 渡された args の数と、元の関数で定義されている引数の数が同じ (func.length) かより多い場合、単にそれを呼び出し func に渡します。
  2. 部分適用を得る: そうでない場合は、func はまだ呼ばれません。代わりに別のラッパー pass が返却されます。これは curried を再度適用して新しい引数と一緒に前の引数を提供します。その後、新しい呼び出しでは、新しい部分適用(引数が不十分な場合)か、最終的に結果が得られます。

例えば、sum(a, b, c) のケースで何が起きるのかを見てみましょう。3つの引数があるので、sum.length = 3 です。

curried(1)(2)(3) に対しては次のようになります:

  1. 最初の呼び出し curried(1) はそのレキシカル環境に 1 を覚え、ラッパー pass を返します。
  2. ラッパー pass(2) で呼び出されます: それは前の引数(1)を取り、渡された (2) と連結し curried(1, 2) を呼び出します。引数の数としては、以前 3 より少ないので、currypass を返します。
  3. ラッパー pass は再び (3) で呼ばれ、次は pass(3) が以前の引数 (1, 2) を取り、3 を追加して curried(1, 2, 3) を呼び出します。 – ついに引数が 3 となったので、それらは元の関数に渡されます。

もしまだ不明瞭であれば、心の中、あるいは紙に呼び出しシーケンスをトレースしてみると良いです。

固定長の関数のみ

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

カリー化よりもさらに

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

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

サマリ

  • 私たちが既存の関数のいくつかの引数を修正するとき、結果となる(汎用さは減る)関数は、部分適用 と呼ばれます。部分適用を得るために bind を使うことができますが、他の方法でも可能です。

    同じ引数を何度も繰り返し指定したくないとき、部分適用は便利です。それは、私たちが send(from, to) 関数を持っていて, from が常に同じになるような場合です。部分適用を得て処理を続けることができます。

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

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

タスク

重要性: 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));
チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。