CLOVER🍀

That was when it all began.

Pythonでカリー化を書いてみる

なんとなく、書いてみたくなったので。

カリー化というのは、複数の引数を取る関数を、最初の引数のみ与えると残りの引数を取る関数に変換すること…です。なんか、書くより例を見た方が早いので…。

例えば、こういう関数があったとして…

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)

内部で引数をリストに追加している形で管理してますからね…。

この時点で、残りの引数を取る関数を「生成」して返すという意味から考えると、この実装はけっこう微妙ですね。また今度、この欠点を埋める実装をちゃんと考えてみることにします。