[Python-ideas] Partial operator (and 'third-party methods' and 'piping') [was Re: Function composition (was no subject)]

Koos Zevenhoven koos.zevenhoven at aalto.fi
Sun May 10 22:06:21 CEST 2015


Reading the recent emails in the function composition thread started by 
Ivan, I realized that my below sketch for a composition operator would 
be better if it did not actually do function composition ;). Instead, -> 
would be quite powerful as 'just' a partial operator -- perhaps even 
more powerful, as I demonstrate below. However, this is not an argument 
against @ composition, which might in fact play together with this quite 
nicely.

This allows some nice things with multi-argument functions too.

I realize that it may be unlikely that a new operator would be added, 
but here it is anyway, as food for thought.  (With an existing operator, 
I suspect it would be even less likely, because of precedence rules : )

So, -> would be an operator with a precedence similar to .attribute 
access (but lower than .attribute):

  # The simple definition of what it does:
  arg->func   # equivalent to functools.partial(func, arg)

This would allow for instance:
  arg -> spam() -> cheese(kind = 'gouda') -> eggs()

which would be equivalent to eggs(cheese(spam(arg), kind = 'gouda'))

Or even together together with the proposed @ composition:
  rms = root @ mean @ square->map     # for an iterable non-numpy argument

And here's something I find quite interesting. Together with 
@singledispatch from 3.4 (or possibly an enhanced version using type 
annotations in the future?), one could add 'third-party methods' to 
classes in other libraries without monkey patching. A dummy example:

from numpy import array
my_list = [1,2,3]
my_array = array(my_list)
my_mean = my_array.mean()  # This currently works in numpy

from rmslib import rms
my_rms = my_array->rms()  # efficient rms for numpy arrays
my_other_rms = my_list->rms()  # rms that works on any iterable

One would be able to distinguish between calls to methods and 
'third-party methods' based on whether . or -> is used for accessing 
them, which I think is a good thing. Also, third-party methods would be 
less likely to mutate the object, just like func(obj) is less likely to 
mutate obj than obj.method().

See more examples below. I converted my examples from last night to this 
IMO better version, because at least some of them would still be relevant.

On 10.5.2015 2:07, Koos Zevenhoven wrote:
> On 10.5.2015 1:03, Gregory Salvan wrote:
>> Nobody convinced by arrow operator ?
>>
>> like: arg -> spam -> eggs -> cheese
>> or cheese <- eggs <- spam <- arg
>>
>>
>
> I like | a lot because of the pipe analogy. However, having a new 
> operator for this could solve some issues about operator precedence.
>
> Today, I sketched one possible version that would use a new .. 
> operator. I'll explain what it would do (but with your -> instead of 
> my ..)
>
> Here, the operator (.. or ->) would have a higher precedence than 
> function calls () but a lower precedence than attribute access (obj.attr).
>
> First, with single-argument functions spam, eggs and cheese, and a 
> non-function arg:
>
> arg->eggs->spam->cheese()   # equivalent to cheese(spam(eggs(arg)))

With -> as a partial operator, this would instead be:

arg->eggs()->spam()->cheese()     # equivalent to cheese(spam(eggs(arg)))

> eggs->spam->cheese  # equivalent to lambda arg: cheese(spam(eggs(arg)))
>

With -> as a partial operator this could be:

lambda arg: arg->eggs()->spam()->cheese()


> Then, if spam and eggs both took two arguments; eggs(arg1, arg2), 
> spam(arg1, arg2)
>
> arg->eggs   # equivalent to partial(eggs, arg)
> eggs->spam(a, b, c)   # equivalent to spam(eggs(a, b), c)

With -> as a partial operator, the first one would work, and the second 
would become:

eggs(a,b)->spam(c)     # equivalent to spam(eggs(a, b), c)

> arg->eggs->spam(b,c)    # equivalent to spam(eggs(arg, b), c)
>

This would become:

arg->eggs(b)->spam(c)     # equivalent to spam(eggs(arg, b), c)

Note that this would be quite flexible in partial 'piping' of 
multi-argument functions.

> So you could think of -> as an extended partial operator. And this 
> would naturally generalize to functions with even more arguments. The 
> arguments would always be fed in the same order as in the equivalent 
> function call, which makes for a nice rule of thumb. However, I 
> suppose one would usually avoid combinations that are difficult to 
> understand.
>
> Some examples that this would enable:
>
>  # Example 1
>  from numpy import square, mean, sqrt
>  rms = square->mean->sqrt  # I think this order is fine because it is 
> not @
>

This would become:

def rms(arr):
     return arr->square()->mean()->sqrt()

>  # Example 2 (both are equivalent)
>  spam(args)->eggs->cheese() # the shell-syntax analogy that Steven 
> mentioned.
>

This would be:

spam(args)->eggs()->cheese()

Of course the shell piping analogy would be quite far, because it looks 
so different.

>  # Example 3
>  # Last but not least, we would finally have this :)
>  some_sequence->len()
>  some_object->isinstance(MyType)
>

And:

  func->map(seq)
  func->reduce(seq)

-- Koos







More information about the Python-ideas mailing list