JavaScript ( 時々 TypeScript ) で学ぶ関数型プログラミングの基礎の基礎 #2 - カリー化について

wakamsha
21

前置き ( ※ 読み飛ばしていただいても OK )

JavaScript は関数型ライクなエッセンスを一部含んではいるものの、決して Haskell のような純粋関数型言語ではありません。JavaScript では変数やオブジェクトの状態を自由に書き換えるようなプログラミングスタイルを通常としているからです。したがって JavaScript で関数型プログラミングをまともに行うのは本来ナンセンスなのかもしれません。しかし関数型の持つ要素の一部だけを取り入れたプログラミングをすることは可能であり、これらを習得することは大規模かつ堅牢な web アプリケーションを設計するのに少なからず恩恵をもたらします。また、 JavaScript には Underscore.js / LodashImmutable.jsRamda.js といった便利なリスト操作ライブラリや RxJS のような非同期処理ライブラリは、 JavaScript で関数型プログラミングをするうえで強力な手助けとなります。

当初は Ramda.js の入門エントリを書くつもりだったのですが、これを理解するには関数型プログラミングの基礎知識が求められます。そこでいきなり Ramda.js と戯れる前に関数型プログラミングの基礎の基礎について学んでみるとしましょう。

シリーズ一覧

当シリーズは関数型プログラミングの全てを習得することを目的としたものではありません。あくまで JavaScript プログラミングに関数型のエッセンスをほんの少し取り入れるところまでを目的とした入門者向けの内容を目指しています。関数型プログラミングを本格的に学びたいという方は、 素直に Haskell や Lisp などを題材に学習されることを強くおすすめします。

🍛 カリー化とは

『関数のカリー化』とは、期待される数より少ない引数で関数を呼び出した場合に、残りの引数を取るためにその呼び出された関数が別の関数を返すような関数にすることを指します。

例えば引数を3つ受け取って結果を返す f(a, b, c) 関数があるとします。この関数に引数を a だけ渡すと結果値の代わりに、足りない引数 bc を引数に持つ g(b, c) 関数を返します。

文章で説明してもややこしいだけなので、サンプルコードを使って理解していきましょう。

function rightAwayInvoker(method: Function, ...targets: any[]) {
  const target = targets.shift();

  return method.apply(target, targets);
}

rightAwayInvoker(Array.prototype.reverse, [1, 2, 3]);
//=> [3, 2, 1]

rightAwayInvoker 関数は、第二引数に渡されたオブジェクトに対して第一引数に渡されたメソッドを即座に実行するというものです。これ自体はなんてことありませんね。ではこの関数をカリー化してみましょう。

import * as _ from 'lodash';

function invoker(method: Function) {
  return function(target) {
    const args = _.drop(arguments);

    return function() {
      return method.apply(target, args);
    }();
  }
}
invoker(Array.prototype.reverse)([1,2,3]);
//=> [3, 2, 1]

rightAwayInvoker をカリー化した invoker関数を定義しました。括弧演算子を2つ記述しているところに注目してください。それぞれに引数をひとつずつ渡しています。つまり関数を二回実行しています。しかし最終結果値は同じ [3, 2, 1]です。ということは invoker 関数は論理的な引数の数 ( 2つ ) が使い切られるまで最終結果値を出すためのメソッド実行を先延ばしにしているということになります。

前回の高階関数の解説エントリで『クロージャ』について触れました。『コンテキストにもとづいて特定の動作を行うように「設定された」関数 ( クロージャ ) を返す』というものです。これと同じ考え方がカリー化された関数にも当てはまります。つまりカリー化された関数は、最終結果を出すために論理的に必要とされる数のパラメータを全て埋め尽くすまで、引数を受け取る度に以前より少しだけ『より設定された』関数を返し続けるということです。

カリー化の方向は左から?右から?

結論から言いますと、今回は『右から』行うようにします ( ※ 理由は後述 ) 。JavaScript は任意の数の引数を渡すことができる言語です ( ex: 可変長引数 ) 。そのため右から左へカリー化することでオプションの引数の個数を固定することが出来ます。例として次の2つの割り算関数を見てみましょう。

function leftCurryDiv(n: number) {
    return (d: number): number => n / d;
}

function rightCurryDiv(n: number) {
    return (d: number) => n / d;
}

引数を一つずつ受け取って最後に割り算結果を返すだけのシンプルなものです。はじめに leftCurryDiv関数を使ってカリー化されたパラメータがどのように結果を返すか見てみましょう。

const divide10by = leftCurryDiv(10);

divide10by(2);
//=> 5

divide10byは「10 / ?」を内部に持った関数です。「?」はカリー化された一番右のパラメータで、次の呼び出し時に値を渡されるのを待っています。二回目の呼び出しで実行されたカリー化関数 ( divide10by ) は、10 / 2を計算して5を返します。

次にrightCurryDivを使ってみましょう。

const divideBy10 = rightCurryDiv(10);

rightCurryDiv(2);
//=> 0.2

leftCurryDivと全く異なる結果を返しましたね。divideBy10関数は「? / 10」を内部に持っており、左側の引数が渡されるのを待っています。

カリー化を右から行う理由は、次回ご紹介する『部分結合』が左から引数の固定化を行うものからです。なのでカリー化を右から左へ行うようにしておけば『左右両方向』からパラメータを固定化出来るため、関数の組み立てがより柔軟なものにできるのです。

カリー化する関数を作ってみよう

divide10by関数とdivideBy10関数の例では手動でカリー化を行いました。Haskell では全ての関数が自動的にカリー化されますが1)任意やデフォルトの引数は存在しません。、JavaScript にはもちろんそのような気の利いた機能はありませんので、このように自前でカリー化対応した関数を定義する必要があります。それではあまりに不便ですので、任意の関数を受け取ってカリー化する関数を作ってしまいましょう。

function curry(fn: (first, second) => any) {
    return function (second) {
        return function (first) {
            return fn(first, second);
        }
    }
}

2段階までカリー化するcurry関数を定義しました。これを利用して先ほどの divideBy10関数と同様の関数を定義すると以下のようになります。

function divide(n: number, d: number): number {
    return n / d;
}

const div10 = curry(divide)(10);

div10(50);
//=> 5

rightCurryDivから定義したdivideBt10と同じ結果となりました。div10関数はその実行部の「? / 10」の ? にあたる一つ目の引数を待機します。

このようにカリー化は JavaScript の関数の動作を『専門化』させることができるというわけです。

やり過ぎに注意?JavaScript におけるカリー化の落とし穴

今回ご紹介したサンプルコードは引数が二つの関数に限定したものでした。Haskell という言語は、関数をデフォルトでカリー化する仕様なのではじめからこの恩恵を受けられますが、JavaScript はこのように自前で定義しなくてはなりません。Haskell と同等の恩恵を受けたいのであれば、引数の数に縛られず任意の段階までカリー化できる関数を作ろうと思うかもしれません。しかし JavaScript には『可変長引数』という仕様があり、これとカリー化が衝突すると混乱を招くリスクがあります。

カリー化は『関数合成』のための便利なアプローチですが、これ以外に『部分適用』もよく使われます。次回は部分適用について学んでいきましょう。

脚注   [ + ]

1. 任意やデフォルトの引数は存在しません。