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

ゆくゆくは有へと

おかゆ/オカ∃/大鹿有生/彼ノ∅有生 の雑記

括弧を憎みすぎた人間の末路(Pythonで関数合成)

本実装はともかく、ちょっと試しにprintしてみたいときあるじゃないですか。そういうときに、

print(f(g(h(1, 2))))

などと、括弧が多すぎて死にそうになることがある。

まあこれくらい我慢しろっても思うんだけど、ちょっと機嫌が悪くてブチ切れた

魔法少女になって括弧を消し去りたいッ!

せめて関数合成を

functoolsモジュールに関数合成のためのツールあるかなと思ったらなかった…作るしかない…。

というわけで作ったのがこれ。Composableクラス。

class Composable:
    def __init__(self, callable_obj):
        self.__callable = callable_obj

    def __call__(self, *args, **kwds):
        return self.__callable(*args, **kwds)

    def __matmul__(self, other):
        @Composable
        def composite_function(*args, **kwds):
            return self(other(*args, **kwds))
        return composite_function

    def __imatmul__(self, other):
        return self @ other
        
    def __getattr__(self, name):
        return getattr(self.__callable, name)

Composable を関数デコレータとして使うことで@演算子を2関数に使うことができるようになる。

@Composable
def add_10(n):
    return 10 + n

@Composable
def product_3(n):
    return 3 * n

print((add_10 @ product_3)(10)) # => 40

これで入れ子の括弧はかなり無くなる!嬉しさがある。

def chain(*func):
    composite = Composable(lambda x: x)
    for func in func_rest:
        composite @= Composable(func)
    return composite

print(chain(add_10, add_10, product_3)(3)) # => 29

ついでにこういうのも作っておいたので、これでなんとかしてもいい。

殲滅させたい

でも括弧を憎みすぎて、もっと括弧を減らしたかった。というわけで魔改造したのがこれ。

class Composable:
    def __init__(self, callable_obj):
        self.__callable = callable_obj

    def __call__(self, *args, **kwds):
        return self.__callable(*args, **kwds)

    def __matmul__(self, other):
        @Composable
        def composite_function(*args, **kwds):
            return self(other(*args, **kwds))
        return composite_function

    def __imatmul__(self, other):
        return self @ other

    def __rshift__(self, other):
        return other(self)

    def __rrshift__(self, other):
        return self(other)

    def __mod__(self, other):
        return self.apply(other)

    def apply(self, *applied_args, **applied_kwds):
        @Composable
        def applied_function(*args, **kwds):
            return self(*applied_args, *args, **applied_kwds, **kwds)
        return applied_function

    def __getattr__(self, name):
        return getattr(self.__callable, name)
@Composable
def xruti(func):
    return func()

@Composable
def add_10(n):
    return 10 + n

@Composable
def product_3(n):
    return 3 * n

@Composable
def add(n, m):
    return n + m

@Composable
def sub(n, m):
    return n - m

@Composable
def product(n, m):
    return n * m


print((sub % 200 @ add % 50 @ product % 4 @ add % 5)(10)) #(1)
print(10 >> add % 5 >> product % 4 >> add % 50 >> sub % 200) #(2)
print(sub % 5 % 1 >> xruti) #(3)

多分3日後の自分は理解できない

まず (1) はさっきと同じで関数合成してるけど、部分適用の % をぶち作った。

(2)では、>> を導入。

こういうときって、「引数にほにゃして、もにゃして、ぽちゃして、どみゃして」って思考なのに関数で書くと逆になるのがイヤ!

ならメソッドチェーンで書けばいいんだけど、Python は絶妙にメソッドチェーンできなくて辛いし…。

なので、>>に左の値を右の関数にぶち込んで値を返すということをしてもらって、括弧をなくしました。

(3) は % がちょっとポンコツで、全部値が埋まってても値を返さずになお関数オブジェクトを返してくるので、xruti関数(ロジバンで "return")を使って値を返すように改造してある。

「キーワード引数」?知らん!!!!!!!!!

お気を確かに

確かに関数で包みまくってると嫌になりますが、まあ流石に上はやり過ぎ感がある。

もう少し穏当にするならまあこんな感じだろうか。

from functools import reduce
def compose(callables):
    callables.reverse()
    def apply(*args, **kwds):
        accum = callables.pop(0)(*args, **kwds)
        return reduce((lambda x, f: f(x)), callables, accum)
    return apply
print(compose([product_3, add_10, product_3, add_10])(10)) #=> 210

引数はアンパックしてもいいかもね。まあこれくらいが平和かも