On Sun, May 24, 2020 at 11:21 AM Steven D'Aprano <
steve@pearwood.info> wrote:
> But how would you go about getting a .__matmul__ attribute onto all
> functions. For ones you write yourselves, you could decorate them at
> definition. What about all the other functions though.
As a functional programming fan, you might not know about this, but we
object oriented programming people have this concept called
"inheritance" where we would add the attribute to FunctionType once, and
just like magic every function would support it!
I think I heard of that inheritance thing somewhere! :-)
My thought is really that there's no way we're going to get .__matmul__ added (with the meaning "compose") to FunctionType. So I was thinking about what one could do with a separate module. I.e. something that could live on PyPI... before, perhaps someday, being added to standard library.
Is there an evil way to monkey patch FunctionType? I.e. without actually recompiling a fork of the interpreter. Alex' forbiddenfruit.curse is pretty cool looking. But it does require explicitly cursing each individual function that might be composed.
forbiddenfruit patches the type, look again. I used a loop because there isn't just one FunctionType: `type(compose) != type(len)`.
Alex Hall:
But seriously, I don't see that much point to this idea. It's just
slightly more concise while not being particularly readable or beginner
friendly.
sorted(paths, key=len @ str)
sorted(paths, key=lambda p: len(str(p)))
I think once you compose three or more functions, lambda starts to look pretty bad.
It does? How? It sounds like the problem isn't the syntax for lambda, but just function composition in general, maybe because of too many parentheses. Maybe you'd like to change:
foo = bar(len(str(path)))
into
foo = (bar @ len @ str)(path)
In any case, it's rare to be able to simply compose three unary functions, e.g. `sorted(paths, key=bar @ len @ str)`. Usually you need to do something slightly more complicated, e.g. a second argument somewhere, and if you throw in functools.partial I think that's easily worse.
This is only slightly mitigated by one of those proposals for "a more concise lambda" that occur intermittently. And if you actually save a composed function under a meaningful name for later use, that much more so.
Why is:
my_op = bar @ len @ str
even more of an improvement over:
my_op = lambda p: bar(len(str(p)))
than @ is an improvement in the previous examples? If the answer is that you're not supposed to assign lambdas, I think the same logic should dictate not assigning compositions.
In fact, the reason to not assign lambdas is for better tracebacks, and now that I think of it, function composition would completely destroy tracebacks. For example:
```
for function in [
lambda p: str(len(p)),
str @ len,
]:
try:
function(3)
except:
traceback.print_exc()
```
Output:
```
Traceback (most recent call last):
File "main.py", line 15, in <module>
function(3)
File "main.py", line 11, in <lambda>
lambda p: str(len(p)),
TypeError: object of type 'int' has no len()
Traceback (most recent call last):
File "main.py", line 15, in <module>
function(3)
File "main.py", line 5, in <lambda>
return lambda *args, **kwargs: self(other(*args, **kwargs))
TypeError: object of type 'int' has no len()
```
So based on that I'm strongly against this kind of proposal without syntactic support.
I've been playing more lately with R Tidyverse. It's pipe with currying of first argument is actually really nice. The pipe operator is god-awful ugly. But other than that it works nicely. For example:
iris %>%
group_by(Species) %>%
summarize_if(is.numeric, mean) %>%
ungroup() %>%
gather(measure, value, -Species) %>%
arrange(value)
It's not abstract composition since it always starts with a concrete object the several operations work on. But it is some of the same feel.