最近、スタートHaskellで「カリー化された関数のメリットは何か?」という質問が出た。そのすぐ後に、kmizuさんがカリー化の誤用に対して警鐘を鳴らしてしていた。僕からするとkmizuさんの「カリー化の定義」も誤用に思えたので、調べるとともに考えたことのまとめ。
いろんな定義
「カリー化する」という用語は、すくなくとも以下の3つの意味で使われているようだ。
- 部分適用という意味
- これは明らかに間違い
- 「複数の引数を取る関数」を「一引数を取る関数のチェインに直す」こと
- これはkmizuさんの定義。世間でもよく使われる。
- 「構造体を一つ取る関数」を「構造体のメンバーを複数の引数にばらし、一引数を取る関数のチェインに直す」こと
- これは僕の定義。というか、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も誤用なんじゃない?」と思っていたので、wikipedia の Currying を読み直してみた。すると、定義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)