Can `->` make a nicer lambda syntax?

I was actually modelling it differently - rather than adding new opcodes or a keyword, just have some kind of functionality to modify the __closure__.

It turns out that this is possible in CPython already by re-creating the function object. I mayyyyyy be voiding the warranty a little :wink: but check it out:

import inspect

# The typing standard module doesn't supply
# "the type of cell objects in closure tuples",
# so fetch it manually:
def _outer():
    x = 1
    return lambda: x

_CellType = type(_outer().__closure__[0])

def hack_closure(func, *names):
    """Preemptively bind names from the caller's scope to a locally-defined
    inner function (/lambda). This way, the inner function will use the
    current values of those variables rather than late-binding to the closure.
    """
    # Access the locals
    caller_locals = inspect.stack()[1].frame.f_locals
    # Replace cells corresponding to the listed names,
    # with newly-created cells storing the same value.
    # The outer function won't update these when its locals change.
    closure = list(func.__closure__)
    old_names = func.__code__.co_freevars
    for name in names:
        closure[old_names.index(name)] = _CellType(caller_locals[name])
    # Clone the original function with a new closure (tuple of cells).
    return type(func)(
        func.__code__, globals(), func.__name__, func.__defaults__, tuple(closure)
    )

Now we can do:

>>> def bind_lazy():
...     return [lambda: _ for _ in range(10)]
... 
>>> def bind_eager():
...     return [hack_closure(lambda: _, '_') for _ in range(10)]
... 
>>> [x() for x in bind_lazy()]
[9, 9, 9, 9, 9, 9, 9, 9, 9, 9]
>>> [x() for x in bind_eager()]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Of course, it could also be written to just use keyword arguments (hack_closure(lambda: _, _ = _), where the keyword argument name _ is taken as the local name to bind, and the value _ is the nonlocal value to use for binding) - accepting strings like this achieves full DRY-ness at the expense of flexibility. To make the syntax nicer, I envision the function becoming a method (maybe .bind) on functions, like (lambda: _).bind('_').

And yes, it’s possible to do something analogous for globals, too. Yes, it’s tricky for a variety of reasons. (No, you cannot leverage collections.ChainMap, at least not in the obvious way. There’s an explicit check for dict type somewhere, but it does accept subtypes, meaning it is possible.) Here’s proof of concept for that:

import inspect

class hacked_globals(dict):
    def __init__(self, d=(), **kwargs):
        super().__init__(d, **kwargs)
    def __missing__(self, name):
        g = globals()
        try:
            return g['name']
        except KeyError:
            return getattr(g['__builtins__'], name)
    def __setitem__(self, name, value):
        if name in self:
            super().__setitem__(name, value)
        else:
            globals()[name] = value
    def __delitem__(self, name):
        if name in self:
            super().__delitem__(name)
        else:
            del globals()[name]

def hack_globals(d=(), **kwargs):
    return lambda func: type(func)(
        func.__code__, hacked_globals(d, **kwargs),
        func.__name__, func.__defaults__, func.__closure__
    )
        
@hack_globals(x=1)
def example_func():
    print(x)
1 Like