Is it possible to do infix function composition in python without wrapping your functions in some decorator?

70 views Asked by At

Title says it all. Seen lots of answers where folks have implemented f @ g, but this requires wrapping f, g in some infix decorator or class. Is it possible to get this to work? Maybe by patching some class that all functions are an instance of or something?

Basically I'd like to get this to work:

f = lambda x: x
g = lambda y: y
def h(a): return a

# <magic>

z = f @ g @ h
assert z(1) == 1

Key here is that the magic above cannot be specific to/reassign f, g, or h.

Edit: Hey whoever closed this and linked that other question, that isn't what I'm asking at all? I am asking about pointfree function composition. More broadly, the answer to their question is yes, and it appears the answer to my question is no. I don't know how on earth this could be a duplicate if that's the case.

2

There are 2 answers

1
Andrej Kesely On

I'm not sure If I get your question right but as you said in your question you can patch the class and add __matmul__ magic method. E.g.:

class A:
    def __init__(self):
        self.v = 10

# patch the A class to work with the `@` operator:
A.__matmul__ = lambda self, other: self.v * other.v

print(A() @ A())

Prints:

100
2
Blckknght On

What you want would require the builtin function type to have a __matmul__ operator. Unfortunately it does not, and you can't add your own methods to builtin types.

This is why you have seen code that wraps the native functions in some other callable type (often using decorator syntax, since it's convenient). Here's my own version of such a decorator:

class Composable:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

    def __matmul__(self, other):
        if isinstance(other, Composable):
            other = other.func
        return Composable(lambda *args, **kwargs: self.func(other(*args, **kwargs)))

You might be able to simplify this code a bit if your functions always have the same kind of signature (e.g. taking one positional-only argument). On the other hand, you might want to make things a bit more complicated and add code to proxy the function's __name__ and __doc__ attributes (and maybe some other attributes). A custom __repr__ method would also be a really good idea.