

Python to Scheme to Assembly (2014) - sea6ear
https://davidad.github.io/blog/2014/02/28/python-to-scheme-to-assembly-1/

======
emmanueloga_
I gather than the author is being hyperbolic in order to emphasize his point
("is easier to reason about correctness with recursion"). But the reason
recursion doesn't "suck" in python (an other languages without TCE), even with
a small call stack, is that usually it is used to tackle problems with a
divide-and-conquer strategy. Even if you are handling a massive amount of
items, if your recursive calls splits them to handle in two or more parts
until there's no more to handle, then it is very hard to deplete the call
stack:

log2 1_000_000_000_000_000 => 49.82892142331043

"Recursion and iteration are equally expressive: recursion can be replaced by
iteration with an explicit stack, while iteration can be replaced with tail
recursion. Which approach is preferable depends on the problem under
consideration and the language used." [1]

BTW, I sympathize with the author's thesis. I was trying to remember
algorithms that are easier to reason about in their iterative form... but I
can't remember any. Conversely, compare recursive vs iterative DFS [2].

1:
[http://en.wikipedia.org/wiki/Recursion_(computer_science)](http://en.wikipedia.org/wiki/Recursion_\(computer_science\))

2: [http://en.wikipedia.org/wiki/Tree_traversal#Depth-
first_2](http://en.wikipedia.org/wiki/Tree_traversal#Depth-first_2)

~~~
harperlee
But only if you can split parts more or less evenly; in other case, numbers go
down quickly...

~~~
emmanueloga_
True, in which case you can take some measures to avoid worst case inputs...
I'm thinking randomization in quicksort, for instance.

~~~
harperlee
I always liked a lot that solution ("adding noise"), it seems very elegant.

------
rpedroso
This article really got me in the mood to play around with Racket again.

Incidentally, I find it hard to square with Guido's belief that recursion
isn't the basis of all programming. Sure, Python has some powerful iterators,
but my CS professors made it clear that iteration is basically just a special
case of recursion.

It might very well be the right decision for Python to prefer iteration, but
to deny the role of recursion in computer science altogether seems misguided.

~~~
iamcurious
I have the same thoughts about list comprehension. The first time I read this
quote I thought Guido was being ironic:

    
    
        "filter(P, S) is almost always written clearer as [x for x in S if P(x)]"

~~~
zem
the comprehension version is actually clearer, though. consider: in filter(P,
S)

1\. which is the predicate and which is the list?

2\. is the list filtered in place (destructively) or is a new list emitted?

3\. does filter mean "select when P" or "reject when P"? both are valid
readings of the english word 'filter', with common non-scitech usage actually
leaning more towards 'filter out'

the list comprehension has none of those ambiguities.

~~~
actsasbuffoon
To be explicit, they're clearer _in Python_ due to the prevailing style and
sensibilities of the community. It's possible to adopt a different set of
guidelines that would eliminate the ambiguity.

For instance, in Haskell the data you're working with is always the last
argument. This stems from the way currying works, which makes life much more
pleasant if the predicate comes first. Ruby also makes it clear, as there's
special syntax for passing an anonymous function to a function call.

For your second point, pure functional languages have complete clarity in this
sort of thing. In Haskell it's obvious that you're returning a new list, and
you couldn't mutate the original even if you wanted to. Ruby has a convention
where ambiguously-mutating function names are suffixed with "!" to indicate
that this version mutates.

As for your third point, I can see where you're coming from, but filter is a
venerable function name. There's a version of filter in most functionally
inspired languages (and a version of map, reduce, etc.) and all the ones I've
seen only keep elements that match the predicate. I suppose this may be why
Ruby calls it "select" instead of "filter". There's also an inverted version
called "reject". All of those also have an in-place variant (select!, reject!,
map!).

My point is that the ambiguity is not unavoidable. These things could easily
be made clearer with different conventions/language-support. Other languages
do quite well with these tools, so it obviously can be done in a clear way.

------
tiffanyh
This is also one of many reasons why people love Lua.

Lua does proper tail recursion automatically.

[http://www.lua.org/pil/6.3.html](http://www.lua.org/pil/6.3.html)

------
Cyberis
This was a great article. I've spent the last year filling in gaps in my own
CS background including CL, Scheme, Racket, CISP, HtDP, etc. This is the first
time I've seen good solid reasoning about Scheme using Assembly and it makes
things really clear. I wish the author would have taken a little more time
reasoning to the correctness of recursion using the y combinator. That's what
he ended up with but it would have been helpful to break that down a bit more.
Also, he mentioned that his next article in that series would have covered
continuations (specifically call-with-current-continuation) and I REALLY would
have enjoyed that since I find continuations a little hard to grasp.

~~~
_callcc
I noticed the implicit y combinator as well... here's something I wrote in
python some time ago to play with the idea:

    
    
        def tco(fn):
            """
            tco
            ---
            
            Usage:
        
            Functions should have the following form:
            
            >>> recursive_fn = lambda fn: lambda x: fn(x+1) if x < 1000000 else x
        
            or
        
            >>> def recursive_fn(fn):
            ...     def recursive_fn_cc(x):
            ...         return fn(x+1) if x < 1000000 else x
            ...     return recursive_fn_cc
            
            The notable difference is that called on their own, these
            functions aren't recursive at all. For example,
        
            >>> recursive_fn(lambda x: x)(1)
            2
        
            This is easier to see in the anonymous-style definition:
        
            >>> lambda fn: lambda x: fn(x+1) if condition(x) else x
        
            So we go from
        
            >>> def recur(x):
            ...     ( function body modifying x )
            ...     if not base_case_reached:
            ...         return recur(x)
            ...     else:
            ...         return x
            
            to
        
            >>> @tco
            ... def recur(fn):
            ...     def _lambda(x):
            ...         ( function body modifying x )
            ...         if not base_case_reached:
            ...             return fn(x)
            ...         else:
            ...             return x
            ...     return _lambda
        
            Then call as usual like
        
            >>> recur(x)
        
            Functions need to take this form as it exposes the fixed-point, 
            namely the recursive `fn`. We need a fixed-point for the modified
            y-combinator. The modified combinator is the same, only where the 
            evaluation step would normally take place, each evaluation is 
            suspended or "thunked" by wrapping in a `lambda`. The thunks
            are then looped over and called in a while-loop, sidestepping
            the recursion depth issue.
        
            TODO: save the kwargs; eliminate the need to write the functions
            as continuations (i.e. `recur = lambda fn:lambda x: ...`) by creating
            new function objects from bytecode, replacing instances of `LOAD_GLOBAL`
            with `LOAD_DEREF`, may need to build temp function in order to create 
            closure so `fn` can be added in `co_freevars`. See e.g.
        
            http://nbviewer.ipython.org/github/bravegnu/python-byte-    code/blob/master/Python%20Byte%20Code%20Hacking.ipynb
    
            """
            
            # y combinator with evaluation step wrapped in throwaway lambda
            # which suspends the evaluation step;
            # important to give `_t_c_o_` or some other highly unlikely keyword
            # argument so we can signal to the loop when stop evaluating;
            # want to be able to *return* a function as well, so checking for
            # '__call__' in directory is not enough.
            # Could also check that argcount is zero, and no other keywords exist;
            # if we're really nuts, generate a uuid or timestamp, then assign 
            # it to `_t_c_o_`
            Y = (lambda f: (lambda x: x(x))(
                   lambda y: f(lambda *args: lambda _t_c_o_=None: y(y)(*args))))(fn)
        
            def tco_cc(*args): 
                _fn = Y(*args)
                while ('__call__' in dir(_fn)) and ('_t_c_o_' in _fn.__code__.co_varnames):
                    _fn = _fn()
                return _fn 
            
            return tco_cc

~~~
_callcc
...re-reading this I realize the comments are a bit of a mess... basically,
it's a decorator that uses the y-combinator to get around the recursion depth
problem by "suspending" the evaluation at each step (I'm sure there's some
technical term for this... thunking?), then later performing each evaluation
inside a while loop. I tested it a bunch and it seemed to work on anything.

I recall being convinced keyword arguments could be made to work, and also
that some byte code hacking would get around the "non-user friendly" bit where
recursive functions need to be written as "functionals" in order to expose the
recursive function itself as a fixed point.

~~~
agumonkey
Reminds me of javascript trampoline
[http://raganwald.com/2013/03/28/trampolines-in-
javascript.ht...](http://raganwald.com/2013/03/28/trampolines-in-
javascript.html) . Was it an inspiration or did you recreate it from scratch ?

~~~
_callcc
It was cobbled together more or less from scratch mainly based on
stackoverflow questions and wikipedia articles. To be honest, it was still
somewhat opaque to me (hence the extraordinarily long comments--trying to
explain it to myself) but the javascript link makes things much clearer!
Thanks

------
hawkice
If your goal is to reason about correctness, recursion can be a useful tool.
Assembly... is not the hammer I'd bust out for that particular reasoning task.

------
sriku
It would've been nice if mathematical induction were indicated as the
foundation of recursion. You're not assuming "it does the right thing" and
then proving "it does the right thing". What you're doing is that you're
proving that "it does the right thing" by showing that "it does the right
thing for N" if "it does the right thing for N-1", and your terminal condition
is something like "it does the right thing for 0". Thus mathematical induction
takes care of both correctness and termination.

------
golergka
> is easier to reason about correctness

> my favorite programming language is x64 assembly

So, author is _really_ used to reading asm and also smarter than an average
developer (and yours truly) or doesn't really care about being able to reason
that much.

(But let me be clear, I'm not being sarcastic and wouldn't be surprised if
he's really 10x genius.)

~~~
gwu78
Are you associating x64 assembly with "smart" or "10x genius"?

How do you make this connection?

Do you think that Python is more complex than assembly or vice versa?

Are you suggesting that being able to program a computer using a subset of a
relatively small, simple language requires more "smart" or "10x genius" than
programming a computer using larger, more complex language?

Please note I am not singling you out, nor am I criticizing your comment nor
your opinion.

I have seen a similar view of assembly expressed many, many times on this
website and I truly am curious how the thought process behind it works.

~~~
golergka
In my experience, assembly is hard to read and reason about. Harder than C,
much harder than python. It doesn't operate in terms in abstractions I, as a
developer, am used to; it doesn't have easy syntax that correlates to logic
flow (blocks and event statements). May be if I spent significant time with
asm, I'll change my mind.

Also, thanks for the disclaimer, although I don't mind if you criticise my
opinion and/or comment. Even if you say that I'm just "too stupid for
assembly", I wouldn't really mind.

------
peter_l_downs
Great article. Fun to read about both Scheme and Assembly at the same time
since I'm currently taking class on each (6.004, 6.945 @ MIT). It's great to
get theory from both ends of the abstraction spectrum.

------
Veedrac
I don't get the argument about why loop invariants are any more complicated
than tail calls. The link doesn't seem to clear it up (the same problem would
occur with the direct translation to tail recursion).

------
baldfat
BASIC --> Assembly --->> Fortran -----> C+- ----> SQL

Now I mostly use R and SQL

~~~
sin7
R --> SQL.

What's a good complement?

~~~
cttet
I actually felt that R functions and data structures are more straight forward
to use than SQL commands. But sadly the data only exists in memory..

~~~
baldfat
The memory issue actually isn't "that bad." There are many different solutions
and Spark and R are looking like amazing.

Here is the announcement link:
[https://groups.google.com/forum/#!topic/apache-spark-user-
mi...](https://groups.google.com/forum/#!topic/apache-spark-user-
mirror/tZs0dHeM6bk)

