No macros in Python

Bengt Richter bokr at oz.net
Mon Dec 16 22:15:08 EST 2002


On Sun, 15 Dec 2002 19:03:23 -0500, Lulu of the Lotus-Eaters <mertz at gnosis.cx> wrote:

>Beni Cherniavsky <cben at techunix.technion.ac.il> wrote previously:
>|IIRC nobody ever said that all macros are evil by definition.
>
>Lulu of the Lotus-Eaters wrote:
>> All macros are evil by definition :-).
>
>Beni Cherniavsky <cben at techunix.technion.ac.il> wrote previously:
>|My point was that sometimes spelling out the repetitive code patterns
>|that macros can abstract over is more evil.
>
>While my previous enunciation had tongue in cheek, I really do believe
>that there is another word, besides 'macros', for "abstraction of
>repetitive code patterns."  They are called 'functions'.  If I find
>myself doing the same thing--or almost the same thing--repeatedly in
>code, I write a function that does that collection of things.
>
>Of course I realize that using functions can have a few extra issues.
>You have to figure out what needs to get passed into the function, and
>what needs to come back out of it.  But in general I would challenge
Ok, I'll try ;-)

ISTM the key issue is being able to write an abbreviation for something
to be done more than once **within the current scope**. An ordinary
function can't do that for several reasons. A nested function can
partially do it, but unlike Pascal (IIRC), you can't rebind/assign to
variables in the enclosing scope, so it's only a partial solution,
and it's also a bad performance hit to set up a call for the sake
of a small function body such as would usually be typical of macros.

A loop construct is a kind of abbreviation that lets you execute a
designated segment of code more than once within the current scope.
You can take advantage of that to create a state machine that selects
arbitrary chunks of code in the current scope for execution in arbitrary
order. Using an explicit state stack, you can get the effect
of local subroutines. And this can work nicely for some problems.
(It also is a convenient structured disguise for spaghetti code with goto's ;-)
I think a pythonic alternative could be possible (see below) but in case someone
isn't familiar with structured spaghetti, here's a toy example:

====< spaghetti.py >==================================================
# spaghetti.py -- bokr at oz.net 2002-12-16
# A serious toy demonstrating structured spaghetti code
# Note that this could be expanded to emulate exceptions and
# other control mechanisms by putting tuples on the control stack
# E.g., the last call, to sqrt, illustrates success/failure return
# selected via callee from alternatives passed on control stack.
# Contrast with returning status on data stack and making caller test
# and branch.
# BTW, if psyco can optimize this, it could be quite fast, IWT.
# Also, it would probably be faster to use ints or single-character state codes
# for the if/elif switch.

# This is a way of writing "subroutines" without subroutine calling
# overhead per se. If one level of overhead is acceptable, you can write
# a similar loop to dispatch to a dynamically varying set of coroutines etc.
# Perhaps I will post a spaghetti task class with single-call overhead,
# and waiting for events etc. It should be fun to see how it looks in Python.

class Sentinel:pass
def spaghetti(start, data=Sentinel):
    if data is Sentinel: datastack = []
    else: datastack = [data]    # for optional use with 'subroutines'
    stack = [start]             # top of state control stack selects next code seg
                                # whether it's "subroutine" or continuation/goto target
                                # start will do a 'return' and empty the stack for normal exit
    def printlog(fmt, *args):
        print (len(stack)*'    ' + fmt) % args  # print indented log messages for demo

    # main state transition dispatching loop
    while stack:
        state = stack[-1]   # not pop: that's for calling and returning
        
        # each if/elif following represents a state and a segment of code
        # (when speed is important, high frequency states should come first)
        if state=='start':  # selecting code seg by name
            # a typical code chunk goes in each if/elif suite
            printlog('Executing start')
            # here we demo a "call" to init
            # first we specify a return continuation point, unless looping (see[2])
            # then we push the subroutine name and let the loop go there via continue
            printlog('Calling init from start')
            stack[-1]='start_1'; stack.append('init'); continue    # typical 'call' pattern

        elif state=='start_1':
            # continuation of start segment, conventionally named
            printlog('Executing start_1')  # we could put this print
            # now we'll call main, which is going to loop n times
            printlog('Calling main from start_1 with single parameter 3')
            datastack.append(3)   # data passing mechanism: main will pop it when done with it
            stack[-1]='start_2'; stack.append('main'); continue     # typical 'call' pattern

        elif state =='start_2':
            printlog('Executing start_2')
            printlog('Returning from start sequence, which should exit (stack = %s)', stack)
            stack.pop(); continue   # typical return when no data passed on data stack

        elif state=='init':
            printlog('Executing init')
            stack.pop(); continue             # typical 'return' pattern

        elif state=='main':
            printlog('Executing main, with count %s on data stack', datastack[-1])
            # a while pattern for executing state 'main'
            if datastack[-1] >0:
                # while cond true
                printlog('count %s > 0', datastack[-1])
                datastack[-1] -=1
                continue    # [2] typical looping pattern, returns to current state
            # while condition false
            printlog('count %s <= 0', datastack.pop())  # note pop -- done with loop count
            stack[-1]='main_1'; continue    # typical transition to successor state

        elif state == 'main_1':
            # demo success/fail return selected from control stack
            x = raw_input('Enter a number for square root (try <0 also ;-) [q to quit]: ')
            if x=='q': stack[-1]='main_2'; continue    # go to main_2
            printlog('Calling sqrt to do square root of %s', x)
            datastack.append(x);
            # here we will define a dual successor to current state which sqrt can select from
            stack[-1]=('main_1_ok','main_1_err');
            # then we push state to "call" as subroutine and let loop go there
            stack.append('sqrt'); continue

        elif state == 'main_1_ok':
            printlog('Got back to main_1_ok, continuing with main_1')
            printlog('Result was %s', datastack.pop())  # note data pop
            stack[-1]='main_1'; continue    # go to main_1 (makes a loop)
            
        elif state == 'main_1_err':
            printlog('Got back to main_1_err, continuing with main_2')
            printlog('Result was %s', datastack.pop())  # note data pop
            stack[-1]='main_1'; continue    # go to main_1 (makes a loop)

        elif state == 'main_2':
            printlog('Executing main_2')
            printlog('Returning from main')
            stack.pop(); continue  # typical return, next on stack is state to "return" to
    
        elif state == 'sqrt':
            import math
            try:
                datastack[-1] = math.sqrt(float(datastack[-1]))
                printlog('Taking success return')
                error = True
            except ValueError, e:
                datastack[-1] = e
                printlog('Taking error return')
                error = False
            # test stack to see if we were called from level 0 (via spaghetti sqrt num)
            # otherwise choose between success/error continuation states and replace
            # control choice tuple with single selected value. Note that multi-way choices
            # could also be implemented easily.
            stack.pop(); stack and stack.append(stack.pop()[error]); continue

        else: # undefined state
            if state !='help': printlog('') or printlog('Undefined spaghetti state "%s"', state)
            print """
Usage: spaghetti.py [state [arg]]
    where state is start by default, but you can try init or main_1 etc
    arg is only used for state==sqrt, so try sqrt number
    This message brought to you by help, or any undefined state.
"""
            break

    printlog('Spaghetti loop exited. stack = %s, datastack = %s', stack, datastack)

if __name__ == '__main__':
    import sys
    args = sys.argv[1:]
    if not args: spaghetti('start') # default test
    elif len(args)==1: spaghetti(args[0])
    else: spaghetti(args[0], args[1])
======================================================================

A log of a default test run, after help:

[19:18] C:\pywk\clp>spaghetti.py help

Usage: spaghetti.py [state [arg]]
    where state is start by default, but you can try init or main_1 etc
    arg is only used for state==sqrt, so try sqrt number
    This message brought to you by help, or any undefined state.

    Spaghetti loop exited. stack = ['help'], datastack = []

[19:18] C:\pywk\clp>spaghetti.py
    Executing start
    Calling init from start
        Executing init
    Executing start_1
    Calling main from start_1 with single parameter 3
        Executing main, with count 3 on data stack
        count 3 > 0
        Executing main, with count 2 on data stack
        count 2 > 0
        Executing main, with count 1 on data stack
        count 1 > 0
        Executing main, with count 0 on data stack
        count 0 <= 0
Enter a number for square root (try <0 also ;-) [q to quit]: 2
        Calling sqrt to do square root of 2
            Taking success return
        Got back to main_1_err, continuing with main_2
        Result was 1.41421356237
Enter a number for square root (try <0 also ;-) [q to quit]: -2
        Calling sqrt to do square root of -2
            Taking error return
        Got back to main_1_ok, continuing with main_1
        Result was math domain error
Enter a number for square root (try <0 also ;-) [q to quit]: q
        Executing main_2
        Returning from main
    Executing start_2
    Returning from start sequence, which should exit (stack = ['start_2'])
Spaghetti loop exited. stack = [], datastack = []
--

Ok, back to the main topic ;-)

The above stays within a single scope, which was the original point ;-)
If there were an alternative to a while statement that would simply
name the loop code and allow that code to be executed within the
**current scope** by an alternate mechanism than control flowing in at
the top of a while or if, then we could have much of the benefit of a macro.

This suggests that there ought to be a way to designate a chunk of code
in the local scope by name and be able to get it to execute some way without
the overhead of an actual subroutine.

E.g., what if you could take an ordinary code "suite" such as would be indented
under, e.g., "if bar:" but instead of the if and its expression, there would
be a keyword and a name which would label the suite, so you could refer to it later.
The suite would be compiled just as if it were under an if statment
(i.e., all symbols refer to the local namespace), but execution would skip over
it like an embedded def (or if 0:).

Importantly, this means that, unlike a nested def of a function, the byte code would
not represent function-building def code that would itself get executed every time
the enclosing function was called. The code would already be built, like if/while/etc
control structure code in the same scope.

Unless invoked, it would not get executed. To execute it, you could spell the invocation
like a parameterless function call or it could be an exec variant. But the code would act
like it got control under an if, as far as scope is concerned.

Since it is much like a def, but different, and always[3] without parameters, perhaps
a bare "def name:" would work (and save picking a new keyword ;-)
Return statements would work as in functions, so you could use parameterless calls
like safe expression macros.

[3] Ok, rather than go through a whole cycle showing parameterless
local code seg execution, I'll just jump to the parameterized version ;-)

To pass parameters, let

    def foo, x, y, z:

mean that x, y, amd z are local to the enclosing scope, as if the above line
were preceded with

    x=y=z=None

Then, to execute the defined foo code suite with **those** x,y,z variables set
to something specific, just write the foo call as if it were an ordinary function.
E.g,

    foo(1, 2, 3)

would be equivalent to

    x=1; y=2; z=3
    foo()

done in the current scope by 1:1 association with the declared parameter sequence.
Note that all parameters act like their existing bindings are default values, so
calling with less than the full set of parameters only binds as many as values supplied.

Now a quick possibly realistic example: Take a routine to plot
from a list of plot command tuples.

def fooplot(plist): # XXX not currently legal Python !!
    def init: # whatever
    def moveto, x, y: return # nothing to do: parameters already bound by "call"
    def rmoveto dx, dy: x+=dx; y +=dy
    def devoords xdev,ydev,x2dev,y2dev: # define transform here is identity xform is not desired
    def lineto, x2, y2: devcoords(x,y,x2,y2); devline(xdev,ydev,x2dev,y2dev); x=x2; y = y2
    def rlineto dx, dy: lineto(x+dx,y+dy)

    init()
    for tup in plist:
        op = tup[0]
        if op=='m': moveto(tup[1],tup[2])      # note that this call would become [4] (approximately)
        elif op=='v': lineto(tup[1],tup[2])
        elif op=='rm': rmoveto(tup[1],tup[2])
        elif op=='rv': rlineto(tup[1],tup[2])
--

    [4] E.g., this calling code should result in "local parameter binding" instead of tuple creation,
        and should do the control via an efficient local call that does not involve frame creation,
        but just pushing a return

            LOAD_FAST                0 (tup)
            LOAD_CONST               2 (0)
            BINARY_SUBSCR
            STORE_FAST               1 (x)
            LOAD_FAST                0 (tup)
            LOAD_CONST               3 (1)
            BINARY_SUBSCR
            STORE_FAST               3 (y)
            LOAD_FAST                2 (moveto)
            CALL_FUNCTION            0              <<-- new functionality to see type of moveto
            POP_TOP                                    \ and just make the local call

and moveto would just be a return that doesn't imply exiting a frame

            LOAD_CONST               0 (None)
            RETURN_VALUE_LOCALLY                    <<-- new fantasized byte code

One could restrict this not to allow a possible return value, and thus get rid of
POP_TOP and LOAD_CONST. It's an option to consider, for speed.

Passing a local code seg reference out of scope might raise difficult
closure issues, so would probably have to be illegal, at first anyway. But that
is shouldn't prevent the benefits of ordinary use. It should be possible to
make references placed outside of the local name slot wrapped so indirectly
exported references can complain if used outside, IWT.

>Cherniavsky, or anyone else, to present a case where anything could be
>done significantly more easily in Python if macros were added.  I really
>do not think there are any such situations.  Not if you consider the
>various facilities for encapsulation already in Python (not just
>functions, but also magic methods on classes, closures, HOFs, and so
>on).
Above I did not propose macros, but I think it might be better than macros
for Python, and could speed things up for some applications ;-)

>
>For all the raves I see about how wonderful Lisp macros are, it always
>seems to amount to a slightly different way of spelling LOOP.  Yeah,
>Python doesn't have an "do..until" construct, and maybe branching could
>be done slightly differently.  But you can do everything these new
>constructs might with no more than an extra line or two of code.  The
>regularity gained by not letting people make their own monstrosity is
>far more important than those couple lines.
>
This has already been commented on ;-)

Regards,
Bengt Richter



More information about the Python-list mailing list