あどけない話

インターネットに関する技術的な話など

カリー化談義

最近、スタートHaskellで「カリー化された関数のメリットは何か?」という質問が出た。そのすぐ後に、kmizuさんがカリー化の誤用に対して警鐘を鳴らしてしていた。僕からするとkmizuさんの「カリー化の定義」も誤用に思えたので、調べるとともに考えたことのまとめ。

いろんな定義

「カリー化する」という用語は、すくなくとも以下の3つの意味で使われているようだ。

  1. 部分適用という意味
    • これは明らかに間違い
  2. 「複数の引数を取る関数」を「一引数を取る関数のチェインに直す」こと
    • これはkmizuさんの定義。世間でもよく使われる。
  3. 「構造体を一つ取る関数」を「構造体のメンバーを複数の引数にばらし、一引数を取る関数のチェインに直す」こと
    • これは僕の定義。というか、Haskellコミュニティの定義。

「部分適用」の意味で使うのは明らかに間違いのなで排除。定義2と3について議論する。あとで、部分適用とは何かに戻る。

カリー化されている

「カリー化する」と言った場合、定義2と3では意味が明らかに違う。すなわち、入力が「複数の引数を取る関数」なのか、「構造体を一つ取る関数」なのかという点が異なる。

しかし、「カリー化されて」いると言った場合、同じ意味を持つ。すなわち、出力された関数は、「一引数を取る関数のチェイン」の形をしている。

言葉で説明すると分かりにくいので、JavaScript を使って例を示す。足し算の + は二項演算子だけれど、二引数の関数だとも考えられる。これを、「一引数を取る関数のチェイン」の形にしてみよう。

var plus = 
function (x) {
    return function (y) {
	return x + y;
    }
}

これは以下のように使う。

(plus(1))(2); → 3

チェインの意味が分かりましたか?

「カリー化されて」いるは、安心して使っていいようだ。

定義2のカリー化する

上記が、まさに定義2のカリー化の例だ。+ という「二引数の関数」を plus という「一引数を取る関数のチェイン」に直したのだから。

定義3のカリー化する

この定義では、対象となる関数は引数として一つの構造体を取る。ここでは、構造体を配列で代用しよう。以下のような関数を考える。

function plusArray(ar) {
    return ar[0] + ar[1];
}

もちろん、以下のように動作する。

plusArray([1,2]); → 3

Haskell には curry という関数があって、これは定義3のカリー化をする。JavaScript で実装するなら、こんな感じになる。

function curry(f) {
    return function (x) {
	return function (y) {
	    return f([x,y]);
	}
    }
}

使ってみよう。

var curryPlusArray = curry(plusArray); 
(curryPlusArray(1))(2); → 3

どっちが正しい?

僕は「定義2も誤用なんじゃない?」と思っていたので、wikipediaCurrying を読み直してみた。すると、定義2も定義3の両方の意味があると書いてあった。

In mathematics and computer science, currying is the technique of transforming a function that takes multiple arguments (or an n-tuple of arguments) in such a way that it can be called as a chain of functions each with a single argument (partial application). I

tuple というのは、無名構造体の意味。

という訳で、「カリー化する」という場合は、文脈によって定義2なのか定義3なのかを判断しないといけないというのが正しいらしい。

多くのプログラミング言語では、関数は「カリー化されて」いない。このような言語の話をしている場合に「カリー化する」と言った場合は、定義2であると解釈すべき。

Haskell では、すべての関数は「カリー化されて」いる。つまり、引数は一つだけで、値か関数を返す。このような世界で「カリー化する」と言った場合は、定義3であると解釈すべき。

カリー化の利点

「定義は分かったけど、結局カリー化すると何が嬉しいの?」と思う人もいるだろう。カリー化のメリットは、部分適用である。ある関数を雛形として、引数をカスタマイズした関数を作り出せる。以下に例を示す。

var plus1 = plus 1;
plus1(2); → 3
var plus2 = plus 2;
plus2(2); → 4

高階関数と組み合わせると、その表記の簡潔さは一目瞭然となる。たとえば、map という高階関数があるとする。

function map(f, ar) {
    var ret = [];
    for(var i=0; i<ar.length; i++) {
	ret[i] = f(ar[i]);
    }
    return ret;
}

以下は、map に plus(1) という部分適用した関数を渡す例。

map(plus(1), [1,2,3]); →[2,3,4]

plus1 という関数を無駄に定義しなくて済んだ。

ちなみに Haskell では、二項演算子も map もカリー化されているので、以下のような記述も可能である。

map1 = map (+1)

Haskellの部分適用は的を射てない

複数の引数を取る関数に対して、いくつかの引数を固定できるなら、これは部分適用と言える。Scala にはこの機能があるそうだ。

しかし、カリー化された関数は、一引数しか取らない。部分なんてない。だから、カリー化された関数に部分適用するというのは、的を射てない表現である。しかし、Haskell の初心者には、こういう風に説明するの分かりやすいらしいので、この表現がまかり通っている。