なんとなく、書いてみたくなったので。
カリー化というのは、複数の引数を取る関数を、最初の引数のみ与えると残りの引数を取る関数に変換すること…です。なんか、書くより例を見た方が早いので…。
例えば、こういう関数があったとして…
def add(x, y): return x + y def add3(x, y, z): return x + y + z
通常、これを呼び出す時は
print add(2, 3) # => 5 print add3(2, 3, 4) # => 9
と書きますが、これらをカリー化したとすると
print add_curried(2)(3) # => 5 print add3_curried(2)(3)(4) # => 9
のような形式で呼び出せるように変換することです。この例だと、add_curriedという関数に2を渡して呼び出すと、引数を1つ取る関数が返ってくるのでそれに3を渡して呼び出している、といった感じですね。
これを素直に実装する方法はいくつかあります。
関数内でローカルな関数を定義して、それを返す。
def add_curried(x): def _add(y): return x + y return _add
lambdaを使用する。
# 関数定義版 def add_curried(x): return lambda y: x + y # 変数定義版 add_curried = lambda x: lambda y: x + y
とまあ、関数の元の定義がわかっていれば、けっこう簡単に書けます。
では、任意の関数をカリー化する関数を実装してみましょう。幸いPythonは動的型付け言語なので、引数の型は気にする必要がありません。これをScalaでやると、ちょっと大変になりそうですね。
とりあえず、簡単のためキーワード引数は考えないことにします。
まず、関数の宣言ですが、カリー化したい関数とカリー化の回数(正確には、元の関数の引数の数)を引数に取ります。そして、カリー化された関数を返す、ということになります。
def curry_n(origin_fun, n): # メソッドの実装 return curried_fun
カリー化された関数を作るためには、引数がひとつの関数を関数の呼び出しで繋いでいく、ということになります。よって、次に呼び出される関数を引数に取り、最初に呼ばせたい関数を生成していけばいいですね。この場合、1番最後に呼び出されるのは、当然のことながら元々の関数であり、繰り返しの回数はカリー化したい引数の数から1を引いたものになります。
よって、こういう形になります。
curried_fun = create_curried_func(origin_fun) # オリジナルの関数を1段階カリー化する for i in range(n - 1): curried_fun = create_curried_func(curried_fun) # オリジナルの関数の引数の数-1だけ、関数を繋ぐ return curried_fun
関数を生成する関数は、次に呼び出されるべき関数を引数に取り、それを関数で包んで返します。この関数が呼び出された時、次に呼び出されるべきが包まれた関数ならそれを返し、そうでなければオリジナルの関数を呼び出すべきなので、そちらを呼び出すようにします。
args = [] # 呼び出し時の引数を溜め込むためのリスト def create_curried_func(next_fun): def __wrapped(x): args.append(x) if next_fun.__name__ != '__wrapped': return next_fun(*args) else: return next_fun return __wrapped
この時、繋げられた関数が受け取った引数は、リストに追加しておきます。そして、オリジナルの関数を呼び出す時に、リストを展開して渡します。
結果、こういう宣言になりました。
def curry_n(origin_fun, n): args = [] # 呼び出し時の引数を溜め込むためのリスト def create_curried_func(next_fun): def __wrapped(x): args.append(x) if next_fun.__name__ != '__wrapped': return next_fun(*args) else: return next_fun return __wrapped curried_fun = create_curried_func(origin_fun) # オリジナルの関数を1段階カリー化する for i in range(n - 1): curried_fun = create_curried_func(curried_fun) # オリジナルの関数の引数の数-1だけ、関数を繋ぐ return curried_fun
あとは、カリー化したい引数の数に合わせて、専用の関数を定義していきます。
# デフォルトは、引数を2個と考える def curry(f): return curry_n(f, 2) def curry_2(f): return curry(f) def curry_3(f): return curry_n(f, 3) def curry_4(f): return curry_n(f, 4)
使い方は、こんな感じです。
from curries import curry, curry_3 def add(x, y): return x + y def add3(x, y, z): return x + y + z print curry(add)(2)(3) # => 5 print curry_3(add3)(2)(3)(4) # => 9
クラスのインスタンスメソッドに使用しても、大丈夫っぽいです。
class Foo(object): def __init__(self, seed = 1): self.seed = seed def add(self, x, y): return self.seed * (x + y) def multiply(self, x, y): return self.seed * x * y f = Foo(3) print curry(f.add)(2)(3) # => 15 print curry(f.multiply)(2)(3) # => 18
ただ、この実装では1つ引数を与えて生成した関数を使いまわすことができません。
add2 = curry(add)(2) print add2(3) # => 5 print add2(4) # => TypeError: add() takes exactly 2 arguments (3 given)
内部で引数をリストに追加している形で管理してますからね…。
この時点で、残りの引数を取る関数を「生成」して返すという意味から考えると、この実装はけっこう微妙ですね。また今度、この欠点を埋める実装をちゃんと考えてみることにします。