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

Line 1: Error: Invalid Blog('by Esehara' )

または私は如何にして心配するのを止めてバグを愛するようになったか

>> Zanmemo

あと何かあれは 「esehara あっと じーめーる」 か @esehara まで

カリー化、部分適用、クロージャという間違いやすい三種についての簡単なメモ書き

追記

カリー化を間違えてカーリー化という表記をしていたのを修正しました。

そもそもカリー化とは何か

複数の引数を取る関数は、一つの引数を取る、関数を返す関数の連続として表現できるということ、と言葉で表現しても抽象的すぎるので、ちょっと式で表してみる。

まず初めにラムダの導入

例として、ある整数に対してプラス1する関数を定義する。このような関数は、{plusone(x) = x  + 1}として表現できる。

ここでこの関数はplusoneという名前を与えられているが、このx + 1という関数そのものを表現するような記法があると便利だろう。そこで、{\lambda}をそのような記法として定義する。

この記法を用いることにより、上記の{plusone(x) = x + 1}{plusone = \lambda(x)x + 1}として表現できるようになる。つまり、関数それ自体を表す記法を導入することによって、関数の名前と、関数それ自体を区別することができるようになる。

カリー化

このような考え方が便利なのは、関数を返す関数というものを表現できるようになるからだ。

例えば、{ladd = \lambda (x).\lambda (y) x + y}という記法は、例えば、{ladd(3) = \lambda(y)3 + y}という関数を返すものとしてとらえらる。このとき、実際に計算したい場合は {ladd(3)(5) = 3 + 5 = 8}というように、二回引数(二つ、ではないところがミソ)を渡すという使い方をする。

このladdは、当然のことながら{add(x, y) = x + y}という、複数の引数を持つ関数としても定義ができる。このとき、{add(x, y) = ladd(x)(y)}と同じ値を返すものとしてみることが可能である。このように、一つの引数を持つ関数の連続によって、複数引数の関数を表現することが可能になる。

理論的にはこういう感じ。少なくとも、上の基本的な概念について抑えておけば、この後に説明する「部分適用」との違いが明確になるだろう。

抽象的なので、ちょっとコードで

では上記の文章をコードに実際に書き直すとどうなるか。例えば、Rubyだと下のようになる:

def add(x, y)
  x + y
end

ladd = ->(x){->(y){ x + y}}

p add(3, 5)
p ladd[3][5]

このように、二つの引数であったaddは、複数のProc(lambdaみたいなもの)に変換できることがわかる。

部分適用との混合

カリー化は、その性質上、部分適用との混合が生まれやすい。例えば、次のようなコードは部分適用ということが可能だ。

ladd = ->(x){->(y) {x + y}}
plus_one = ladd[1]
p plus_one[3]

カリー化と部分適用の関係は、カリー化があくまでも「複数の引数を取る関数は、一つの関数を取る関数の連続として定義できる」ということが焦点になっている。そして、重要なのは、カーリー化された結果として、部分適用が可能になるという区別である[要マサカリ]。

クロージャーとはちょっと違うの?

上のコードを見たとき、少しだけ「あれ、これってクロージャみたいなものじゃないの」という感じは、自分の中ではあった。

確かに、カリー化自体は、クロージャによって実現することが可能であるとは言える。実際に、例示されたコードはクロージャ(つまり内部の引数のスコープを後続の関数が引き継ぐ)であることは間違いないのだが、何度も言っているようにカリー化はそういうことができる、つまり「複数の引数を持つ関数を、一つの引数を持つ関数の連続として表現できる」ということであって、それをどう実装するか、ということとは別問題であると思う[要マサカリ]。

Rubyにおけるカリー化

Rubyにおいては、既にProc#curryというのメソッドが存在しているため、先ほどの関数(正確にいうとRubyは全部メソッドなので……という面倒臭い言い訳がいる)も、カリー化することが容易だ。

def add(x, y)
  x + y
end
ladd = method(:add).to_proc.curry
p ladd[3][5]

ちなみに、Rubyのメーリングリストではそのやりとりがあってわかりやすかったので、そこにもポインタを貼っておきます。

まとめ

最近、計算論を勉強していて、ちょっとこのあたりについてそういえば自分も混合していたところがあったなあと思ったので、簡単にメモをしておいた。もし何かの参考になれば幸いである。

合わせて読みたい

蛇足

ちなみに今回はRubyをサンプルとして書きましたが、下の意見もあり:

一方、カリー化(を行う関数)と部分適用(を行う関数)では静的な型が異なるため、静的型付き言語ではこのような勘違いは起きづらいです。カリー化という用語の正しい使い方について調べるときは、動的型付き言語のサンプルコードを用いた解説ではなく、できる限り静的型付き言語(Haskell, OCaml, Standard ML)を用いた解説を読む事をお勧めします。

この辺りを意識するといいかもしれません(逆に言うと、Rubyのような言語だと、このような区別に対して無頓着になりがちであるというところなのでしょう)。

Haskellはもともとカリー化されているので、自分的には説明しにくい感じもありますが、試しにコードを書いてみると、下のようにかけるでしょう:

-- Preludeのソースコードより
-- https://hackage.haskell.org/package/base-4.7.0.2/docs/src/Data-Tuple.html#curry
curry :: ((a, b) -> c) -> a -> b -> c
curry f x y  =  f (x, y)

add :: (Integer, Integer) -> Integer
add (x, y) = x + y

plus_one :: Integer -> Integer
plus_one = curry add 1
-- `plus_one 2` で 3 が取り出せる

plus_oneのところが、実際に部分適用しているところで、curryというのが実際にカーリー化する部分です。ここでも明確なように、カリー化とは、カリー化されていない関数、この場合だと二つの引数を渡さないといけない関数を、一つずつ適用できる引数として変換することが、型のところからわかり、部分適用は単に引数を渡してあげているということがわかります。

参考書籍

論理と計算のしくみ

論理と計算のしくみ

計算論 計算可能性とラムダ計算 (コンピュータサイエンス大学講座)

計算論 計算可能性とラムダ計算 (コンピュータサイエンス大学講座)

関数プログラミング入門 ―Haskellで学ぶ原理と技法―

関数プログラミング入門 ―Haskellで学ぶ原理と技法―