Technologicat / unpythonic

Supercharge your Python with parts of Lisp and Haskell.
Other
91 stars 3 forks source link

Pythonify TAGBODY/GO from Common Lisp #45

Open Technologicat opened 4 years ago

Technologicat commented 4 years ago

See Peter Seibel: Practical Common Lisp, Chapter 20 for an explanation.

Rough draft of how we could pythonify this. User interface:

from unpythonic.syntax import macros, with_tags, tag, go

@with_tags     # <-- decorator macro
def myfunc():  # <-- just a regular function definition
    x = 42
    tag["foo"]  # tag[...] forms must appear at the top level of myfunc
    x += 1
    if x < 10:
        go["foo"]  # go[...] forms may appear anywhere lexically inside myfunc
    return "whatever"

In a with_tags section, a tag[...] form at the top level of the function definition creates a label. The go[...] form jumps to the given label, and may appear anywhere lexically inside the with_tags section. To stay within Python semantics (following the principle of least surprise), myfunc otherwise behaves like a regular function.

Possible macro output:

# actually no explicit import; just use `hq[]` in the macro implementation.
from unpythonic import trampolined, jump

def myfunc():  # may take args and kwargs; we pass them by closure
    @trampolined
    def body():  # gensym'd function name
        nonlocal x
        x = 42
        return jump(foo)
    def foo():  # use the tag name as the function name
        nonlocal x
        x += 1
        if x < 10:
            return jump(foo)
        return "whatever"
    return body()
    x = None  # never reached; just for scoping

Essentially, this is another instance of lambda, the ultimate goto.

Notes:

Things to consider:

Technologicat commented 4 years ago

Updated draft for macro output, supporting nested with_tags sections:

# actually no explicit import; just use `hq[]` in the macro implementation.
from unpythonic import trampolined, jump

# define in unpythonic.syntax.tagbody; unhygienic_expose to have it available at the macro use site
class JumpToTag(Exception):
    def __init_(self, tag):
        self.tag = tag
        # uncaught error message
        self.args = ("go[] to a label '{}' not defined in any lexically enclosing with_tags section".format(tag),)

# macro output
def myfunc():  # may take args and kwargs; we pass them by closure
    # define our set of tags (capture these in the syntax transformer)
    our_tags = {"foo"}  # gensym'd variable name on LHS
    # top-level code of myfunc, split into helper functions
    # Each must be trampolined, because a go[] from an inner with_tags
    # (if two or more are nested) may jump into any of them.
    @trampolined
    def body():  # gensym'd function name for part before first tag
        nonlocal x
        x = 42
        return jump(foo)
    @trampolined
    def foo():  # use the tag name as the function name
        nonlocal x
        x += 1
        if x < 10:
            return jump(foo)
        return "whatever"
    # runner harness
    # Copy locals to have a guaranteed-frozen copy of the current state,
    # so we retain references to the helper functions even if the user code
    # overwrites those names.
    our_funcs = dict(locals())  # gensym'd variable name on LHS
    f = body  # gensym'd variable name on LHS
    while True:
        try:
            return f()
        # catch nonlocal jumps from inner nested with_tags sections
        except JumpToTag as jmp:
            if jmp.tag in our_tags:
                f = our_funcs[jmp.tag]
            else:  # not ours, let the jump propagate further out
                raise
    # never reached; just for scoping top-level locals of myfunc
    x = None
Technologicat commented 4 years ago
Technologicat commented 4 years ago
Technologicat commented 4 years ago

Meh, maybe we should just go for the previous idea, using exceptions. That gives better semantics. Nesting feels pretty important and a goto should act like a goto (even if it can only jump within the same level or outward).


Technologicat commented 4 years ago

I had hoped to get this into 0.14.3, but the technical cleanup has already produced enough material for a minor update. Better to push that out first, and then concentrate on these additions.