読者です 読者をやめる 読者になる 読者になる

uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかFregeとかJavaとか -

「何だ今日はカレーか」「いえ、カレーライスです。カレーは部分的よー。」

groovy

curry化の話。

GroovyのClosure#curry()は「カリー化」じゃなくて部分適用だと言われる事があります。なんでそうなんだという話を私の理解している限りで説明してみます。

まず、カリー化というのはラムダ算法方面の話で、関数適用に関する概念です。たとえば、f(x,y)という二引数の関数があったとします。そして、「二引数の関数は嫌だ」と思ったとします。

なんで二引数関数が嫌かというと、数学的に扱いやすくするため1引数の関数の話に一般化したいからです。
一般化がそんなにしたいのかよ、とも思いますが、常に一引数関数の話にも落とせるようにしておくのは何かと便利なのでしょう。まあ、動機付けの話はそれで良しとしましょう。

さて、2引数の関数f(x,y)を、1引数の関数gで表現するには、「f(x,y)」という関数適用について、それを「g(x)(y)」で代用できるようなgを、fを基にしてなんとかして編み出せればよいわけです。これがカリー化です。カリー化を実際にどうやるかという説明は本記事では割愛します。

ここでgは、fからカリー化によって作り出された関数です。fをカリー化することで、gを導きます。留意点は、gは「関数を返す関数」だと言う事です。1引数yをとる関数を返しているのですから。ここをもうちょっと詳しく言うと、g(x)(y)を分解すると

gg = g(x)
gg(y)

ということで、g(x)は、ggという関数を返し、それに引数yを与えて評価しています。これが「gはggという関数を返す関数」という事です。

これに対して、「カリー化操作」は、fからgを導いているのですから、「関数を返す関数を返す関数」だといえます*1。少なくとも1段レベルが違う話です。

さて問題は、Groovyのcurryは、上でいうg(x)の部分の適用をやってくれるだけなので、カリー化操作ではないということです。

いったんまとめると、

  • (1)カリー化は、多引数関数を、1引数関数に変換する操作。(たとえば、fをgに変換する)
  • (2)カリー化によって得られた1引数関数gに、g(1)みたいに具体的な引数を与えて、引数の値を固定する操作を部分適用と呼ぶ。「部分」の意味は、当初の多引数関数の引数の一部だけが部分的に決定しているから。関数適用過程の一部が実行されるというイメージ。
  • (3)部分適用を適宜繰り返していくことで、最終的には全引数が固定され、カリー化される前の関数fが返したであろう値を最終的には得る事ができる

んで、繰り返しになりますが、GroovyのClosure#curryは、fを基にして、「多引数関数(クロージャ)fの引数の値を一部与えた呼び出しを実行するクロージャ」を作ってくれ、これは上で言う(2)の「g(1)」なのですが、これを得る事自体はカリー化操作ではありません*2

さて、以上より、本来ならば「gを得る操作」がcurryという名前を持ったメソッドの機能であるべきだろ、と言われると反論できません。ただ、「真のカリー化操作」をするAPIなりメソッドの有用性は、じっさいのところはGroovyにおいてはたいして無いわけで(あるかもしれませんが)、部分適用に(他に良い名前が思いつかないから)curryという名前を(そのものではないかもしれないが関連する概念として)付けてしまえという判断(意図的な判断があったかどうかは知りませんがもしあったとして)が、実用言語のAPI設計をする立場から、言語同断なあり得ない判断か、というと特にはそうとも思いません。数学的に厳密でない事が犯罪なら副作用を持ったコードを「関数」と呼んだり、代入を表すのに等号(=)使ったりした人は逮捕される事になるわけで。まあ、いずれにせよ、別に私が名前をつけたわけじゃないから言い訳をする必要もないんですけどね。

結論としては、「Closure#curry()は(名前はカリーですが実際には)関数の部分適用を行うメソッドです」と説明しておくのが無難ですかね。

*1:3個以上の引数なら、g1つではなく複数の関数に分解される。

*2:さらに言うと、その操作の中に実際にカリー化操作が明示的に含まれているとも限りません。おそらくGroovyのClosure#curryの実装コード中では、明示的なカリー化に対応するコードは存在しないと思います(未確認ですが)。