カリー化された関数

qiitaの週間ランキング?の上位に、カリー化された関数の話が出ていたけど、分かりにくいなぁと思ったので、自分なりに記事を書いてみる。

ちなみにカリー化された関数は英語訳するとcurried functionになる。

Standard ML of New Jersey という言語がある。その言語は、メジャーな言語と比べるとなんか癖が強いと思うけど、言語として、いろんな特徴が実装されているように思える。その 1 つにカリー化がある。

smlnjをインストールすると、インタープリタが実行できるようになる。

% sml
Standard ML of New Jersey v110.85 [built: Sat Dec 22 16:51:02 2018]
- val a: int = 2 + 3;
val a = 5 : int

例えばこんな風に。

関数の定義は、

- fun add a b: int = a + b;

こういう構文になる。

呼び出しはこう。

- add 2 4;
val it = 6 : int

ところで、smlnjは、静的な型付言語で、関数を定義すると、型が出力される。

- fun increment (a:int) : int = a+1;
val increment = fn : int -> int

この場合、incrementという変数に、ラムダ式を代入したような、表記が出力されており、int型の引数を渡すとint型の値が帰ってくるようなのが書かれているのがわかると思う。

引数が増えると、どうなるのか予想して見て欲しい。 最初、僕は

- fun add a b: int = a + b;
val add = fn : int, int -> int

こうかと思ってた。でも、違った。

- fun add a b: int = a + b;
val add = fn : int -> int -> int

実際はこうだった。 int -> int -> int は、わかりやすく書くと、int -> (int -> int)のことで、これは、実際に、1 つの引数でも呼べることを意味している。

- val add2 = add 3;
val add2 = fn : int -> int

これを宣言しておくと、

- add2 5;
val it = 8 : int
- add2 19;
val it = 22 : int

こんな風な呼び出しができるようになる。つまり、部分適用ができるようになる。

もちろん、僕が最初に思ったような書き方もできる。

- fun add (a,b) = a+b;
val add = fn : int * int -> int

タプル(tuple)を使用すると最初に思ったような関数を宣言することができる。

こんな風に、部分的な動作をする関数を返すことができる関数をキャリー化された関数というらしい。

正確には、wikiとかをみると、写像とかの詳しい話が出ていて、部分適用が行いやすくなるという程度にとどまっている。

wikiを読むと、カリー化と部分適用を混同するなよって書いてある。

部分適用は、あくまで、add(3)でも呼べるようにすることで、カリー化は、add(x)とした時の動作を規定することって書いてある(ように感じた)。