Apply operator ( f @ x means f(x) )

This is conflating application and composition.

f ∘ (g ∘ h) = (f ∘ g) ∘ h means (f ∘ (g ∘ h))(x) = ((f ∘ g) ∘ h)(x), which falls out naturally when you then convert these to f((g(h(x)))) and f(g(h(x))).

But you are proposing function application, not composition, and f(g(x)) != f(g)(x). For application f @ g @ x == f @ (g @ x) != (f @ g) @ x.

An operator cannot mean both apply and compose. If it means apply, it needs to be right-associative.

4 Likes

Sorry for being misleading . I see now how this can happen. f(g) usually has no sense in programming , other then to send a function pointer/reference of g to the function f. To be not ambiguous, I meant it to be right-associative, as it is much more useful. That is probably apply operator. Though I managed to be confused by

In this case @ seems to be composition rather than application.

I don’t agree:

buyoperations = collections.OrderedDict @ map @  (lambda x: (x[0],x) , self.get_buy_operations_with_adjusted @ sorted @ items ) =

                        collections.OrderedDict @ map @  (lambda x: (x[0],x) , self.get_buy_operations_with_adjusted @ (sorted @ items) )  =

buyoperations = collections.OrderedDict @ map @  (lambda x: (x[0],x) , self.get_buy_operations_with_adjusted @ (sorted(items)) )  =

buyoperations = collections.OrderedDict @ map @  (lambda x: (x[0],x) , self.get_buy_operations_with_adjusted(sorted(items)) )  =

buyoperations = collections.OrderedDict @ (map @(lambda x: (x[0],x) , self.get_buy_operations_with_adjusted(sorted(items)) ))  =

buyoperations = collections.OrderedDict (map( (lambda x: (x[0],x) , self.get_buy_operations_with_adjusted(sorted(items)) )))  

It does raises the question if f @ (a,b) is
f(a,b) or f ( tuple(a,b))

Having it the first form is much more useful. But I do understand how the second form can be attributed to right-associativity.

Regarding:

However, in python it’s left associative and that can not be changed in a backwards compatible way

I now understand. Well, maybe it is better to use @@ for this. Or do you have other ideas? & could work maybe.

I don’t quite understand this whole discussion. We can already do any composition we like by defining a decorator or some meta-function that applies any number of functions in sequence, in a pipeline, why would you want more? So why not simply do sth like this (or work it out as nice/general as you need it to be):

def compose(*args):
    def composed():
         res = None
         for a in args:
             res = a(res)
         return res
    return composed

c = compose(func1, func2, func3)
c(None)

And, on the other hand, using ‘@’ as built-in composition operator would break numpy code - or force lots of code that is already out there to be rewritten.

I use this method as well, and prefer it for having to explicitly name each pipeline step and have them laid out top to bottom. And it works nicely with generators.

I don’t find operators for either compose or apply compelling in a language without generalized currying / partial application.[1] And as a personal stylistic preference, I think it is very easy to overuse e.g. Haskell’s . or $ [2] and leave yourself with inscrutable one liners of unmaintainable operator soup. :slight_smile:


  1. …and I know you can implement currying as a function in Python, or use functools.partial, but then you have another nested function call or another operator on top of however you’re implementing composition. ↩︎

  2. …not to mention >>= and all the others I can’t remember anymore, but they’re not really relevant to the proposal. ↩︎

2 Likes

Codon makes use of the pipe operator. I have found it very helpful; you can give it a try.

1 Like

Seeing this right as Mojo gets released is… painful, hahah.

How so? I tried looking through their docs but didn’t find anything related to function composition

Nice code, I will use it in my repo . I guess it is quite simple.

My code:

def c(*cargs):
    def composed(*args,**kwargs):
        res = cargs[-1](*args,**kwargs)
        for a in cargs[:-1][::-1]:
            res = a(res)
        return res
    return composed

def rc(*args):
    return c(*args)()

Example:

c(sorted,list,map)(lambda x:x*2 , [6,4,5])
1 Like

I believe the ideal kind of compose function would have its arguments curried by default, otherwise there is literally no gain in elegancy because you have to constantly use functools.partial. So, something like this should be possible:

def add_ten(x: float) -> float: 
    return x + 10

run_funcs = compose(filter(function=None), map(function=add_ten), list)

I doubt this kind of function can be trivially implemented in plain Python.

I would agree with @flyinghyrax here - composition without currying probably isn’t really that beneficial.

1 Like

Yes, the composition is right-associative and inconsistent with the language.

I don’t advocate adding the @ operator to the language, but if it were added, I think it would be more reasonable for f@x to mean x(f) rather than f(x).

From wiki: Pullback (differential geometry),
the function composition can be represented as f \circ g(x)=f(g(x))=g^*f(x) which is called “pullback”.
Generally, f(g(h(x)))=f \circ g \circ h(x)=(h^* g^* f)(x).
In the real world of differential geometry, pullback, such as Lie dragging operation is a fundamental calculus.
In the programming language, it could be expressed as f(g(h(x))) == x @ h @ g @ f like pipes.

Similar to the decorator style:

@f
@g
def h(x): ...

can be interpreted as a pullback function (h @ g @ f) as a left-associative operator against left side operand x.

I had not heard of pullback before, and now I have a name for this concept! Thank you. :slightly_smiling_face:

Now I have made it:

In[38]: C /  list / map % (lambda x: x*2) @  range(1,5)
Out[38]: [2, 4, 6, 8]

work

So, / is composition , % is partial (nice trick) and @ is applying.

buyoperations = C / collections.OrderedDict / map % (lambda x: (x[0],x)  
 /  self.get_buy_operations_with_adjusted /  sorted @ items 

^ Actually similar thing worked. Pretty cool.

& is partial but in lower precedence .
^ is applying with lower precedence

So,

In [109]: C / list/ zip &  C / list  @ range(5) ^ [4,8,9,10,11]
Out[109]: [(0, 4), (1, 8), (2, 9), (3, 10), (4, 11)]

is

list (  partial ( zip , (  list(range(5)) ))   ( [4,8,9,10,11])    )

The full code:

from typing import Callable, TypeVar
from typing_extensions import ParamSpec

T = TypeVar('T')
P = ParamSpec('P')
Q = ParamSpec('Q')
U = TypeVar('U')

from typing import Generic
class CInst(Generic[P,T]):
    def __init__(self,func :Callable[P,T]):
        self.func=func 
    def __truediv__(self,other :Callable[Q,U]) -> CInst[Q,T] :
        def inner(*args: Q.args, **kwargs: Q.kwargs) -> T:
            return self.func(other(*args,**kwargs))
        return CInst(inner)

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

    def __xor__(self,other):
        return self.__matmul__(other)

    def __call__(self,*args: P.args, **kwargs: P.kwargs) -> T:
        return self.func(*args,**kwargs)

    def __matmul__(self,other) -> T:
        if (type(other) == tuple):
            return self.func(*other)
        elif (type(other) == dict):
            return self.func(**other)
        else:
            return self.func(other)

    def __mod__(self, other):
        if (type(other) == tuple):
            f= partial(self.func,*other)
        elif (type(other) == dict):
            f= partial(self.func,**other)
        else:
            f=partial(self.func, other)
        return CInst(f)




class CSimpInst(CInst):
    def __init__(self):
        self.func = None 
    def __truediv__(self,other :Callable[Q,U]) -> CInst[Q,U] :
        return CInst(other)
    def __call__():
        raise NotImplemented() 


C=CSimpInst() 

A low precedence right to left operator could have been nice…
Full code with currying is at https://github.com/eyalk11/compare-my-stocks/blob/master/src/compare_my_stocks/common/composition.py

1 Like

Neat! Not my style personally, but I think it’s very cool that you got this syntax working with just existing language features. Nice work.

1 Like