[Python-ideas] Function composition (was no subject)

Andrew Barnert abarnert at yahoo.com
Sun May 10 07:24:00 CEST 2015


On May 9, 2015, at 20:08, Ron Adam <ron3200 at gmail.com> wrote:
> 
>> On 05/09/2015 06:45 PM, Andrew Barnert via Python-ideas wrote:
>>> On May 9, 2015, at 08:38, Ron Adam<ron3200 at gmail.com>  wrote:
>>> >
>>> >
>>> >
>>> >On 05/09/2015 03:21 AM, Andrew Barnert via Python-ideas wrote:
>>>>>> >>> >I suppose you could write (root @ mean @ (map square)) (xs),
>>> >
>>>> >>Actually, you can't. You could write (root @ mean @ partial(map,
>>>> >>square))(xs), but that's pretty clearly less readable than
>>>> >>root(mean(map(square, xs))) or root(mean(x*x for x in xs). And that's
>>>> >>been my main argument: Without a full suite of higher-level operators
>>>> >>and related syntax, compose alone doesn't do you any good except for toy
>>>> >>examples.
>>> >
>>> >How about an operator for partial?
>>> >
>>> >          root @ mean @ map $ square(xs)
> 
>> I'm pretty sure that anyone who sees that and doesn't interpret it as
>> meaningless nonsense is going to interpret it as a variation on Haskell and
>> get the wrong intuition.
> 
> Yes, I agree that is the problems with it.
> 
>> But, more importantly, this doesn't work. Your square(xs) isn't going
>> to  evaluate to a function, but to a whatever falling square on xs returns.
>> (Which is presumably a TypeError, or you wouldn't be looking to map in the
>> first place). And, even if that did work, you're not actually composing a
>> function here anyway; your @ is just a call operator, which we already have
>> in Python, spelled with parens.
> 
> This is following the patterns being discussed in the thread.  (or at least an attempt to do so.)
> 
> The @ and $ above would bind more tightly than the ().  Like the doc "." does for method calls.  

@ can't bind more tightly than (). The operator already exists (that's the whole reason people are suggesting it for compose), and it has the same precedence as *.

And even if you could change that, you wouldn't want to. Just as 2 * f(a) calls f on a and then multiplies by 2, b @ f(a) will call f on a and then matrix-multiply it by b; it would be very confusing if it matrix-multiplied b and f and then called the result on a.

I think I know what you're going for here. Half the reason Haskell has an apply operator even though adjacency already means apply is so it can have different precedence from adjacency. And if you don't like that, you can define your own infix operator with a different string of symbols and a different precedence or even associativity but the same body. That allows you to play all kinds of neat tricks like what you're trying to, where you can write almost anything without parentheses and it means exactly what it looks like. Of course you can just as easily write something that means something completely different from what it looks like... But you have to actually work the operators through carefully, not just wave your hands and say "something like this"; when "this" actually doesn't mean what you want it to, you need to define a new operator that does. And, while allowing users to define enough operators to eliminate all the parens and all the lambdas works great for Haskell, I don't think it's a road that Python should follow.

> But the evaluation is from left to right at call time.  The calling part does not need to be done at the same times the rest is done.  Or at least that is what I got from the conversation.
> 
>     f = root @ mean @ map & square
>     result = f(xs)

But that means (root @ mean @ map) & square. Assuming you intended function.__and__ to mean partial, you have to write root @ mean @ (map & square), or create a new operator that has the precedence you want.

> The other examples would work the same.

Exactly: they don't work, either because you've got the precedence wrong, or because you've got an explicit function call rather than something that defines or references a function, and it doesn't make sense to compose that (well, except when the explicit call is to a higher-order function that returns a function, but that wasn't true of any of the examples).

>>> >Actually I'd rather reuse the binary operators.  (I'd be happy if they were just methods on bytes objects BTW.)
>>> >
>>> >          compose(root, mean, map(square, xs))
> 
>> Now you're not calling square(xs), but you are calling map(square, xs),
>> which is going to return an iterable of squares, not a function; again,
>> you're not composing a function object at all.
> 
> Yes, this is what directly calling the functions to do the same thing would look like.  Except without returning a composed function.

I don't understand what you mean. The same thing as what? Neither directly calling the functions, nor your proposed thing, returns a composed function (because, again, the last argument is not a function, it's an iterator returned by a function that you called directly).

>> And think about how you'd actually write this correctly. You need to
>> either use lambda (which defeats the entire purpose of compose), or partial
>> (which works, but is clumsy and ugly enough without an operator or
>> syntactic sugar that people rarely use it).
> 
> The advantage of the syntax is that it is a "potentially" (a matter of opinion) alternative to using lambda.  

Not your syntax. All of your examples that do anything just call a function immediately, rather than defining a function to be called later, so they can't replace uses of lambda. For example, your compose(root, mean, map(square, xs)) doesn't define a new function anywhere, so no part of it can replace a lambda.

The earlier examples actually do attempt to replace uses of lambda. Stephen's compose(root, mean, map square) returns a function. The problem with his suggestion is that map square isn't valid Python syntax--and if it were, that new syntax would be the thing that replaces a need for lambda, not the compose function. Which is obvious if you look at how you'd write that in valid Python syntax: compose(root, mean, lambda xs: map(square, xs)).

I've used the compose(...) form instead of the @ operator form, but the result is exactly the same either way.

> And apparently there are a few here who think doing it with lambda's or other means is less than ideal.

I agree with them--but I don't think adding compose to Python, either as a stdlib function or as an operator--actually solves that problem. If we had auto-curried functions and adjacency as apply and a suite of HOFs like flip and custom infix operators and operator sectioning and so on, then the lack of compose would be a problem that forced people to write unnecessary lambda expressions (although still not a huge problem, since it's so trivial to write). But with none of those things, adding compose doesn't actually help you avoid lambdas, except in a few contrived cases. (And maybe in NumPy-like array processing.)

> Personally I'm not convinced yet either.
> 
> Cheers,
>  Ron
> 
> _______________________________________________
> Python-ideas mailing list
> Python-ideas at python.org
> https://mail.python.org/mailman/listinfo/python-ideas
> Code of Conduct: http://python.org/psf/codeofconduct/


More information about the Python-ideas mailing list