HOME 備忘録 書き込む

身も蓋もない Curry 化の実装


Feb 14, 2012

1. Curry 化の定義

Curry 化というのは、複数の引数を取る関数を1つの引数をとる関数の列に置き換えることです。 たとえば、3 つの引数をとる f(x,y,z)fcurried(x)(y)(z) の形式に変換することです。

fcurried(x) は1つの引数をとる関数を返し、 その関数を呼び出すと、また1つの引数をとる関数 (fcurried(x)(y))を返し、 さらにその関数を呼び出す (fcurried(x)(y)(z)) と、もともとの関数 f(x,y,z) を呼び出したときの結果を返します。

また、Curry 化した関数は使いまわすことが可能です。

p = fcurried(x)(y)
p(z1) --> f(x,y,z1)
p(z2) --> f(x,y,z2)
詳しくは Curry (Wikipedia) を見てください。

2. 1 個以上の引数をとり、引数の数が固定されている任意の関数を Curry 化する関数の実装方法

1 個以上の引数をとり、引数の数が固定されている任意の関数を Curry 化する関数 curry を作ってみます。

Curry 化についてはいろいろな人がいろいろな実装を試していますが、結構ごたごたしているので、 もっと簡単な方法はないか考えてみました。 その結果、以下に述べるような身も蓋もないやり方がシンプルに、かつ (コードは) エレガントに 実装できることに気づきました。

Curry 化された関数は、

  1. もともとの関数と、確定した引数のリストを保持し、1つの引数をとる関数として呼び出されることができる。
  2. 関数として呼び出されると、与えられた引数を、保持している引数のリストに加え、
つまり、 Curry 化された関数から Curry 化された関数を作る関数を呼び出すことでシンプルに実装できます。

3. Python での実装 (1) クラスを定義する

上で述べた方針に従って、Curry 化された関数のクラスを Python で書いてみます。

[curry0.py]

001:   #!python3.2
002:   
003:   from inspect import getfullargspec
004:   from copy import  copy
005:   
006:   
007:   
008:   def cons(ls, x):
009:       ls1=copy(ls)
010:       ls1.append(x)
011:       return ls1
012:   
013:   
014:   class Curry :
015:       '''class of Curried function'''
016:   
017:       def __init__(self, fun, av=[]):
018:           self.fun = fun
019:           self.ac = len(getfullargspec(fun).args)
020:           self.av=av
021:           assert len(self.av) < self.ac
022:   
023:   
024:       def __call__(self, val):
025:           av = cons(self.av, val)
026:           return self.fun(*av) if len(av) == self.ac else Curry(self.fun, av)
027:   
inspect モジュールの getfullargspec() を使うと関数がどのような引数をとるのか調べることができます。 通常の引数は argspec オブジェクトの args というリストに格納されているので、その長さを調べると引数の個数がわかります。 Curry 化された関数のオブジェクトは もともとの関数 (self.fun)、その関数がとる引数の数 (self.ac)、及び確定した引数のリスト (self.av) を保持しています。
Curry 化された関数のオブジェクトが1引数の関数として呼ばれると、与えられた引数を保持していた引数のリストに加え、その長さが self.ac に等しいときは もともとの関数に確定した引数を適用した結果 (self.fun(*av)) を返し、 そうでなければ もともとの関数と新たな確定した引数のリストから Curry 化された関数のオブジェクト (Curry(self.fun, av) を作ってそれを返します。

curry0.py を import して試してみると以下のようになり、正常に動作しているのがわかります。

>>> from curry0 import Curry
>>> def sub3(x,y,z): return x-y-z
...
>>> csub3 = Curry(sub3)
>>> csub3(10)(2)(3)
5
>>> p=csub3(10)(5)
>>> p(1)
4
>>> p(3)
2
また、次のようにデコレータとして用いることができます。
001:   @Curry
002:   def sub3(x,y,z):
003:       return x-y-z

4. Python での実装 (2) 高階関数を使う

メソッドとして __init__() と __call__() しか持たないクラスは高階関数を使うと簡潔に書けます。 curry0.py の 14 行目以降を高階関数で書き直すと以下のようになります。 10 行で Curry 化を実装することができます。

[curry.py] (抜粋)

001:   def curry(fun, av=[]):
002:   
003:       ac = len(getfullargspec(fun).args)
004:       assert len(av) < ac
005:       
006:       def curried(x):
007:           av1=cons(av, x)
008:           return  fun(*av1) if len(av1) == ac  else curry(fun, av1)
009:   
010:       return curried
curry.py を import して試すと以下のようになり、正常に動作しているのがわかります。
>>> from curry import curry
>>> def sub3(x,y,z): return x-y-z
...
>>> csub3 = curry(sub3)
>>> csub3(10)(2)(3)
5
>>> p=csub3(10)(5)
>>> p(1)
4
>>> p(3)
2
Curry クラスと同様にデコレータとして用いることができます。
001:   @curry
002:   def sub3(x,y,z):
003:       return x-y-z

5. Scheme での実装

MIT-Scheme での実装を以下に示します。 arty-process は 関数の引数を返す関数です。 MIT-Scheme 独自の関数ですが、他の Scheme の実装でも同様の機能があります。
001:   (define (curry fun . rest)
002:     (let ((av (if (null? rest) '() (car rest)))
003:           (ac (car (procedure-arity fun)))) ;; MIT-scheme only
004:       (lambda (x)
005:         (let ((av1 (cons x av)))
006:           (if (= ac (length av1))
007:               (apply fun (reverse av1))
008:               (curry fun av1))))))
Scheme の実装は Python の実装よりコードがごてごてした感じです。 Python は関数型言語としてもいけているといえます。

6. 終わりに

呼ばれるたびに引数を溜め込んで、必要なだけ溜まったらもともとの関数を適用するという身も蓋もないやり方で Curry をごく簡単に実装することができました。

ソースはこちらからどうぞ。