Currying syntax with multiple parameter lists

Flat is better than nested.
I remember being confused a lot when I was learning how to write my first decorator. Now eight years later, with the last two spent full-time writing Python, I introduced a bug in production because I forgot to return the inner function from within a decorator. By introducing multiple parameter lists when defining a function, Scala does a great job at improving readability for curried functions (https://docs.scala-lang.org/tour/multiple-parameter-lists.html). In Python, that could look like e.g.: def log(level=logging.DEBUG, logger=logging.root)(func)(*args, **kwargs): logger.log(level, 'call: %s', func.__qualname__) return func(*args, **kwargs) Which would be sugar for: def log(level=logging.DEBUG, logger=logging.root): def _log(func): def __log(*args, **kwargs): logger.log(level, 'call: %s', func.__qualname__) return func(*args, **kwargs) return __log return _log The obvious problem in this example, is that `functools.wraps` is missing. One solution would be for me to flex my 8 years of experience, and present these two (super/meta-)decorators: def wraps_decorator(decorator: Callable[[F], F]): @functools.wraps(decorator) def _wraps_decorator(func): return functools.wraps(func)(decorator(func)) return _wraps_decorator @wraps_decorator def wraps_decorator_factory(decorator_factory)(*args, **kwargs): return wraps_decorator(decorator_factory(*args, **kwargs)) Applying the latter on the first example, it becomes @wraps_decorator_factory def log(level=logging.DEBUG, logger=logging.root)(func)(*args, **kwargs): logger.log(level, 'call: %s', func.__qualname__) return func(*args, **kwargs) which is equivalent to: def log(level=logging.DEBUG, logger=logging.root): def _log(func): @functools.wraps(func) def __log(*args, **kwargs): logger.log(level, 'call: %s', func.__qualname__) return func(*args, **kwargs) return __log return _log Implementation-wise, I think it's very feasible since it's only sugar. And I'm sure that the required grammar changes are possible with the shiny new parser. And considering PEP 659, I can imagine that this syntax is beneficial because the closure variables coincide with the parameter lists.

On Tue, May 25, 2021 at 04:01:35AM -0000, Joren Hammudoglu wrote:
Imagine how confused you would be if they had included multiple parameter lists. Not only would have needed to learn: * functions as first class values * function decorators * decorator syntax * factory functions but in addition: * what multiple parameter lists mean * something that looks like a non-nested function actually being implicitly nested. That's fifty percent increase in complexity. I keep coming across not just beginners, but even experienced Python programmers who don't get decorators, and sometimes even ask "why do we need them?". I'm not sure that making decorators even more complex is going to make things easier for them.
Ouch! I feel your pain. But honestly, "I don't test my changes before pushing them out onto production systems" is not going to be solved by more syntax.
Well, that's one interpretation. It's certainly more *compact* and *terse*, and saves some boilerplate, but I'm not sure that it's more readable. IMNSHO it's got all the readability advantages of jargon initialisms over regular words in sentences. *wink*
Indeed. The even bigger problem is that it is only narrowly applicable. In the most general case, nested functions can contain `2*N + 1` blocks of code, where N is the level of nesting: # two levels of nesting def factory(arg): block 1 # setup before making decorator def decorator(func): block 2 # setup before making inner function def inner(*args, **kw): block 3 block 4 # post-process inner function return inner block 5 # post-process decorator return decorator but your sugar only allows a maximum of one block no matter how deep the nesting goes: def decorator(arg)(func)(*args, **kw): block 3 I can't say that I've ever needed all five blocks for a two-level nested decorator, but I've commonly needed two or three, and occasionally four, of those blocks. Even ignoring functools.wraps, I don't think I've ever had a two-level decorator that was just a straight pass through of def outer(): def middle(func): def inner(): block 3 return inner return middle with no setup or post-processing at all. At least, no examples are coming easily to mind. So I think that this sugar is only useful in a fairly narrow set of circumstances. And to make it usable, you need to add not just one but two meta-decorators, so we can use wraps in the right place:
One solution would be for me to flex my 8 years of experience, and present these two (super/meta-)decorators:
As a beginner to decorators, I found functools.wraps hard to wrap my head around (pun intended). If I had to cope with your wraps_decorator_factory instead, I probably would have just decided that the whole decorator concept was an example of Architecture Astronauts Gone Wild. You're basically saying that your sugar, far from making it easier to write decorators that do what we want, makes it *harder*, so we need a decorator-decorator-decorator to give use the functionality back that we lost by using your sugar. And then, after all that, it's still not going to prevent you from accidentally pushing out a buggy decorator into a production system without testing it thoroughly first :-( Going back to your syntax, it's not clear where docstring belong to: def decorate(spam=5, eggs=1)(func)(*args, **kwargs): """I'm a docstring. But whose docstring am I???""" block If it's treated as a docstring for the inner function it will be lost if you use wraps, so we might not want to do that. But using wraps is not mandatory -- if you don't use it, you might want the docstring to be attached to the inner function. On the third hand, if we attach it to the inner function, there's no easy way to document the outer factory functio. And on the fourth hand(!), if the docstring is attached to the outer factor function, as the indentation suggests, that adds yet another oddity to learn. Both the docstring and the block are physically indented one level, but one is conceptually indented three levels in to the implicit inner function, and the other isn't. -- Steve

On Tue, May 25, 2021 at 04:01:35AM -0000, Joren Hammudoglu wrote:
Imagine how confused you would be if they had included multiple parameter lists. Not only would have needed to learn: * functions as first class values * function decorators * decorator syntax * factory functions but in addition: * what multiple parameter lists mean * something that looks like a non-nested function actually being implicitly nested. That's fifty percent increase in complexity. I keep coming across not just beginners, but even experienced Python programmers who don't get decorators, and sometimes even ask "why do we need them?". I'm not sure that making decorators even more complex is going to make things easier for them.
Ouch! I feel your pain. But honestly, "I don't test my changes before pushing them out onto production systems" is not going to be solved by more syntax.
Well, that's one interpretation. It's certainly more *compact* and *terse*, and saves some boilerplate, but I'm not sure that it's more readable. IMNSHO it's got all the readability advantages of jargon initialisms over regular words in sentences. *wink*
Indeed. The even bigger problem is that it is only narrowly applicable. In the most general case, nested functions can contain `2*N + 1` blocks of code, where N is the level of nesting: # two levels of nesting def factory(arg): block 1 # setup before making decorator def decorator(func): block 2 # setup before making inner function def inner(*args, **kw): block 3 block 4 # post-process inner function return inner block 5 # post-process decorator return decorator but your sugar only allows a maximum of one block no matter how deep the nesting goes: def decorator(arg)(func)(*args, **kw): block 3 I can't say that I've ever needed all five blocks for a two-level nested decorator, but I've commonly needed two or three, and occasionally four, of those blocks. Even ignoring functools.wraps, I don't think I've ever had a two-level decorator that was just a straight pass through of def outer(): def middle(func): def inner(): block 3 return inner return middle with no setup or post-processing at all. At least, no examples are coming easily to mind. So I think that this sugar is only useful in a fairly narrow set of circumstances. And to make it usable, you need to add not just one but two meta-decorators, so we can use wraps in the right place:
One solution would be for me to flex my 8 years of experience, and present these two (super/meta-)decorators:
As a beginner to decorators, I found functools.wraps hard to wrap my head around (pun intended). If I had to cope with your wraps_decorator_factory instead, I probably would have just decided that the whole decorator concept was an example of Architecture Astronauts Gone Wild. You're basically saying that your sugar, far from making it easier to write decorators that do what we want, makes it *harder*, so we need a decorator-decorator-decorator to give use the functionality back that we lost by using your sugar. And then, after all that, it's still not going to prevent you from accidentally pushing out a buggy decorator into a production system without testing it thoroughly first :-( Going back to your syntax, it's not clear where docstring belong to: def decorate(spam=5, eggs=1)(func)(*args, **kwargs): """I'm a docstring. But whose docstring am I???""" block If it's treated as a docstring for the inner function it will be lost if you use wraps, so we might not want to do that. But using wraps is not mandatory -- if you don't use it, you might want the docstring to be attached to the inner function. On the third hand, if we attach it to the inner function, there's no easy way to document the outer factory functio. And on the fourth hand(!), if the docstring is attached to the outer factor function, as the indentation suggests, that adds yet another oddity to learn. Both the docstring and the block are physically indented one level, but one is conceptually indented three levels in to the implicit inner function, and the other isn't. -- Steve
participants (2)
-
Joren Hammudoglu
-
Steven D'Aprano