コンテンツにスキップ

0554: シグネチャによる自動カリー化

問題

カリー化とは、多引数関数を、引数を1つずつ受け取る関数の連鎖に変換する操作です。 集合の言葉では、\(f : X \times Y \to Z\)\(\mathrm{curry}(f) : X \to (Y \to Z)\) に対応させる全単射 \(Z^{X \times Y} \cong (Z^Y)^X\) にあたります。 Haskell などの関数型言語では、すべての関数が最初からカリー化されており、引数の一部だけを与える部分適用が自然に書けます。

デコレータ curry を実装してください。

  • @curry を付けた関数は、必要な位置引数がすべて揃うまで、部分適用された関数を返し続けます。
  • 引数は1個ずつでも複数個まとめてでも渡せます。渡した位置引数の総数が仮引数の個数に達した時点で、元の関数を呼び出して結果を返します。
  • 仮引数の個数は inspect.signature() で取得します。
  • functools.wraps を使い、元の関数のメタデータを引き継ぎます。

制約

  • 対象関数の仮引数は位置引数のみとし、デフォルト値、可変長引数(*args**kwargs)、キーワード専用引数は扱いません。
  • 対象関数の仮引数は1個以上とします。
  • カリー化後の関数はキーワード引数では呼び出されないものとします。
  • 位置引数を仮引数の個数より多く渡した場合は TypeError を送出します(元の関数の呼び出しで自然に発生するもので構いません)。
  • 部分適用しても、元の関数や既存の部分適用の状態は変化しないものとします(下の例の add_1_2 を参照)。

>>> @curry
... def add3(x, y, z):
...     return x + y + z
>>> add3(1)(2)(3)
6
>>> add3(1, 2)(3)
6
>>> add3(1)(2, 3)
6
>>> add3(1, 2, 3)
6
>>> add_1_2 = add3(1, 2)
>>> add_1_2(10)
13
>>> add_1_2(100)  # 部分適用は何度でも再利用できる
103
>>> add3.__name__
'add3'
>>> volume = curry(lambda a, b, c: a * b * c)  # デコレータ構文を使わない適用
>>> volume(2)(3)(4)
24
>>> add3(1, 2, 3, 4)
Traceback (most recent call last):
    ...
TypeError: ...

発展

デフォルト値やキーワード引数を持つ関数にも対応するように curry を拡張してください。 inspect.signature()Parameter.kindParameter.default を調べ、必須の位置引数が揃った時点で元の関数を呼び出すようにします。

参考

  • 『Python Distilled』第5章「イントロスペクション、属性、シグネチャ」
  • 『Python Distilled』第5章「デコレータ」
  • カリー化 - Wikipedia