Introduce support for trailing closures

In many languages trailing closures are a feature designed to improve readability when you need to pass a function a reference to another function to act as a callback, as part of constructing a DSL dynamically or part of functional programming.

Currently I believe the way Python handles these kinds of paradigm goes against the first four lines of PEP 20

Firstly, if you want a function to have a callback you have two options:

  1. Pass in the name of a function
def callback(*args):
    print("do some thing")

other_function(callback)

In this example it’s easy to read but in practice the developer now needs to keep this function and the thing using it, co-located. If code is sufficiently complicated you now need to scroll up and down, as well as keep track of which variables hold values and which are just references to functions.

  1. Lambdas
other_function(lambda : print("do something"))

This is succinct and keeps it co-located. But python limits it to one line meaning you will end up with a very long line if you tried to do it just using a lambda. Even if python allowed for multiple lines you would end up with “Callback Hell” where the code ends up being hard to read as a result of the the code for the lambda being in the middle of where you are specifying the parameters for the function using that closure.

Secondly, It makes some DSLs more cumbersome to write

Imagine you need to make a decision of what needs to be built at runtime depending on some kind of dynamic value.

This often needs you to write a load of if statements / construct a list of nodes which are then passed into other nodes. Some tools such as Gradio require you to pass in a reference to the function which will update the values of those nodes.

For example here is an example of constructing a markup like document:

document = Node()]
home = document.create_node()

if has_messages:
    list = home.create_node()
    for message in messages:
       message_node = list.create_node()

You can use context managers to assist with making it more markup like:

if has_messages:
   with home.create_node() as list:
       for message in messages:
          message_node = list.create_node()

The problem is that the code in the with block has to be executed along side the code where it is defined, if the DSL only wants to evaluate this part of the tree (For example imagine you get a new message), it cannot. It must evaluate all of it. This is why some tools like Gradio require you to pass references to functions which update some sections of your UI.

How can this be done today

Trailing closures can already be done using Python today, but the syntax isn’t pythonic.

Firstly we must create a closure which will setup a function to accept a trailing closure:

def closure(f):

    def activator(*args, **kwargs):

        def wrapper(closure):
            return f(closure, *args, **kwargs)

        return wrapper

    return activator

We now apply that decorator to our filter function:

@closure
def filter(closure, array):
    yield from [i for i in array if closure(i)]

With this closure decorator decorating our “filter” function now get a new decorator function which will accept arguments to be passed to the original decorated function. When it’s used to decorate the closure, it will pass the closure to and call the original function

names = ['James', 'Josh', 'Micheal']

@filter(names)
def filtered_names(name: str):
    return name.startswith('J')

print(list(filtered_names))

Now wie’ve decorated our closure function with the decorated function which accepts the trailing closure. Our original function will now call our closure when it needs.

Due to how python works, when a decorator returns a value, that value will be set at a variable with the same name as the decorated function. Hence why we can print the filtered names as if it was originally a variable.

Unfortunately it’s not very pythonic as unlike @property, it’s not obvious to the use why the decorated function has turned into a filtered list.

However this implementation does have good symmetry with the existing functional methods python has like filter since closure is passed in as the second argument like it is today. In fact since filter is a decorator we can do the same

filter(lambda: False, names)

The Proposal

My proposal is that I would like to make this a first-class element of python’s syntax, I’ve hopefully demonstrated that this can be already supported in python with minimal changes or having to introduce large changes to how the python language functions.

This proposal will introduce, zero new keywords (That’s right!) and hopefully wouldn’t need a closure decorator as proposed above.

I propose that a trailing closure can be indicated by using “with” after a function’s call site. The trailing closure would be passed in as a reference (like functions and lambdas) as the first argument for functions or the second for class methods.

Meaning all the function signature needs to do is to accept a function as it’s first argument (like python’s functional methods such as filter and map already do)

The syntax would work like this, a function call, the “with” keyword followed by the lambda syntax for describing args (and types). The body works with same rules as a python function (Use return to return, yield to yield etc etc)

filter(names) with item: str -> bool:
    return item.startswith("j")

And thats it, we would be going from:

filter(lambda item: item.startswith("j"), names) 

or

def j_only(item)
   return item.startswith("j")

filter(j_only, names) 

to just this

filter(names) with item: str -> bool:
    return item.startswith("j")

Much easier to read and follow the control flow of the program. In other words more pythonic!

Backwards Compatability:

Existing code would still work since previous Python Versions the functions would just be standard functions which accept a reference to a function or a lambda as their first argument.

This would just be some syntactical sugar for the new python.

1 Like

I quite liked the approach mentioned in New syntax `Trailing Block` for constructing objects with complex structure - #66 by AlfredDU, which is to work on precompiler.

If I understood correctly, this way everyone can design such conveniences for their own taste.

Also, it would be easy to play with such ideas and maybe those who are liked by many would find their way to the core language.

As soon as it’s ready I’ll be sure to build it.

1 Like

Note that there have been a few past ideas that use a trailing block to allow some variation of a multi-line lambda syntax (either directly, or by allowing full nested function definitions):

@in would be the closest in spirit to the proposal in the current thread:

names = ['James', 'Josh', 'Michael']

@in filtered_names = [n for n in names if relevant_name(n)]
def relevant_name(name: str):
    return name.startswith('J')

print(list(filtered_names))

So far, even without considering the question of whether or not they address a problem worth solving, they’ve all failed the “effort vs expressiveness” test (that is, the potential increase in language expressiveness they might offer isn’t considering to be high enough to justify the effort required to fully design, implement, document, and maintain them, including the additional cognitive burden they would place on all users of the language). And I say that as the author of two of the draft ideas on my list (they’re Deferred indefinitely because I know the answer I would get if I actually submitted them would be “No”, and I’d agree with that answer given the weak justifications those proposals currently contain)

1 Like

Thank you dg-pb!

The precompiler approach is quite popular in other language community like javascript. Some idea which may be seem too radical in the first glance, but after the precompiler come out and been applied, developer start to accept it as a powerful & reasonalbe language patch. Finally some are accpeted to the official language features.

Dear Alyssa: it is a pleasure that more Core Developers join the disscuss!

As for my proposal of new syntax “trailing block”, it has a more ambitious objective than introducing trailing closure to python – I think one can define customized indent block behavior with this syntax. And thus, it seems more “failed in effort vs expressiveness” for the first glance :grinning: :grinning: :grinning:

The effective way, as I think, to prove or disprove the cons and pros of introducing the new syntax, is the precompiler. With the precompiler, native python codes and polyfilled python codes can be compared in some scenarios. The proposal may then start from being a dialect of python language, a further step from just ideas.

Look into those previous PEP proposals I think for me they struggle mainly because they are in the middle between mine and @AlfredDU 's proposals.

They way more complex than my proposal but also way less expressive than New syntax `Trailing Block` for constructing objects with complex structure - #66 by AlfredDU

In my first post I demonstrated that the Python of today technically supports trailing closures through the use of decorators, this means if this proposal were to be implemented. It could be kept simple by effectively recreating what these decorators are already doing.

Most of the complexity is in the addition of the new syntax to make it easier to follow than how it can be currently done in python through decorators (it’s just sugar essentailly), which I’ve again tried to keep simple thorough using already existing constructs from python:

  • The use of with to indicate you want to do something within the context of another thing. If a standard with clause means you are running code within the context of a context manager, then a with after a function call site means you are defining code to be ran within the context of that function. I understand the concerns about reusing the same keyword in different ways but I still think it’s spiritually consistent
  • It uses the existing lambda argument and keyword declaration for getting the arguments, meaning there is no additional fancy syntax for defining the function. Lambda functions have already proven their expressiveness
  • And the code body itself follows the same rules etc as if you defined a function block, again a proven concept.

As such, I consider this most incremental version of trailing closures possible in python. Which should hopefully make sure the effort part of the equation is minimal compared to those other proposals.

Of course I’m happy to implement this in a preprocessor or build a custom python which demonstrates it’s use.