Wednesday 21 August 2024

Python Pipes and Infix Notation

I recently came across a discussion about adding support for UFCS to Python. As expected, such idea was dismissed, but it gave me some good food for thought, as I had never heard before about this Uniform Function Call Syntax thing. So the idea is:

Allows any function to be called using the syntax for method calls (as in object-oriented programming), by using the receiver as the first parameter and the given arguments as the remaining parameters.

No major language implements this feature. In Kotlin we have a couple of things slightly related: Extension Functions and Function Types with Receiver. I can get a reference to an existing function and type it as a Function Type with Receiver, and in that case I can invoke that function reference both through a receiver, or passing it over as the first parameter.

The main interest of UFCS for me is for chaining function calls (aka pipes) and for that I would prefer a pipe operator. In this previous post I mentioned a very interesting project where they use this a combination of 2 operators: |> as a pipe operator. For that to work functions using that new operator have to be decorated with a decorator that will rewrite its source. That's a pretty amazing and crazy approach, but there's a more simple one, using a wrapper object (to wrap the functions being piped) that overrides the binary operator | (__ror__). There's an amazing project that does that, and more! The main Pipe class is just like this:


class B:
    def __init__(self, f=None, *args, **kw):
        self.f = f
        self.args = args
        self.kw = kw


class Pipe         (B): __ror__ = lambda self, x: self.f(x, *self.args, **self.kw)


And we use it like this:


def transform(st: str) -> str:
    return st[0].upper() + st[1:]

def clean(st: str, to_remove: list[str]) -> str:
    for it in to_remove:
        st = st.replace(it, "")
    return st

def wrap(st, wrapper: str) -> str:
    return f"{wrapper}{st}{wrapper}"

print("this is not Asturies" 
    | Pipe(transform)
    | Pipe(clean, ["not"])
    | Pipe(wrap, "--")
)

# --This is  Asturies--

Let's see a typical use case with iterables, mapping, filtering...



cities = ["Paris", "Prague", "Lisbon", "Porto"]
print(cities 
    | Pipe(lambda items: map(str.upper, items))
    | Pipe(lambda items: filter(lambda x: x.startswith("P"), items))
    | Pipe(list)
)

# ['PARIS', 'PRAGUE', 'PORTO']

That's a bit verbose, so to improve that the (very) smart guy behind pipe21 added extra classes for all the common use cases (Filter, Map and many more). With that, we can rewrite the above code like this:


cities = ["Paris", "Prague", "Lisbon", "Porto"]
print(cities 
    | Map(str.upper)
    | Filter(lambda x: x.startswith("P"))
    | Pipe(list)
)

# ['PARIS', 'PRAGUE', 'PORTO']

I should mention that the almighty coconut language comes with support for pipes (indeed it supports multiple pipes operators).

Kotlin supports operator overloading, but it has a more limited set of operators that can be overloaded, and "|" is not among them, so it seems we can not port this approach.

Related to this I've come across another pretty amazing use of operator overloading (and the callable concept), enabling infix notation for function calls. I copy-paste below the code:


from functools import partial

class Infix(object):
    def __init__(self, func):
        self.func = func
        
    def __or__(self, other):
        return self.func(other)
        
    def __ror__(self, other):
        return Infix(partial(self.func, other))
        
    def __call__(self, v1, v2):
        return self.func(v1, v2)

>>> @Infix
... def add(x, y):
...     return x + y
...
>>> 5 |add| 6
11

>>> instanceof = Infix(isinstance)
>>>
>>> if 5 |instanceof| int:
...     print "yes"
...
yes

>>> curry = Infix(partial)
>>>
>>> def f(x, y, z):
...     return x + y + z
...
>>> f |curry| 3

>>> g = f |curry| 3 |curry| 4 |curry| 5
>>> g()
12        
        

I find particularly appealing the isinstance example. We are converting the isinstance function in a sort of instanceof operator that being used to javascript instanceof operator, and kotlin is operator (in Kotlin, as in JavaScript, reference equality is checked with ===) seems more natural to me. This idea is pretty amazing cause we are using operator overloading (that can only be applied to a predefined set of operators) to create sort of new operators!!!

No comments:

Post a Comment