Main

November 3, 2009

Function calls are not stack frames

Tim Bray is spreading more misinformation about tail recursion. He describes it this way:

It looks like a subroutine call, but in the case where it occurs as the last thing in the routine, it magically, silently, and automatically gets turned into, now how did I put it? “A highly controlled and structured GOTO.”

A tail-call is a subroutine call. The efficient implementation does not magically transformed into something else; if it doesn't create a stack frame on such a call, it's because one simply isn't relevant.

The essential observation behind the efficient-tail-call implementation (not "optimization"—more on which in a moment) is as follows: For most programming languages, a stack frame is needed not for a subroutine call but only for an argument evaluation, that is, an evaluation whose result is temporary and needs further processing. Calls in the middle of a procedure are "argument" evaluations, because their results need further processing. It's really the temporary, non-final natural of the result that forces us to do the book-keeping that remembers where to come back to.

Another confusion is between semantics and cost model:

Call me old-fashioned, but it just seems wrong when the semantics of two identical function invocations vary wildly as a function of whether there’s an executable statement between the invocation and the function exit.

The semantics of the call doesn't change; the result and the side-effects are the same (that's what's usually meant by "semantics" anyway). The cost, on the other hand, might be quite different depending on whether a stack frame is needed.

Unfortunately, efficient tail recursion has often been described as a transparent "optimization," so that it might or might not be efficient and the programmer can't tell in advance.

Efficient tail calls (or space-consuming ones) really should be part of the official "cost model" of the language, something that goes along with the semantics, as a peer to the semantics, in fact. The cost model tells you how expensive you can expect things to be, but should be a little less binding than the semantics, since language implementors should have some freedom to do additional optimizations.

The idea that stack frames should correspond directly to the call structure is just odd. Maybe we want to know the call structure at runtime; in that case, we should capture that as debugging information, or as a reflection feature of the runtime system, but not as a core language design. Let the language implementation use the stack as efficiently as possible!

To summarize:

  • "Tail-call optimization" is a terrible misnomer.
  • The programmer should have assurance as to whether tail-calls have a cost.
  • Most languages have no reason to use up space on every function call; only calls whose result will be fed to another expression need to use space.

May 20, 2009

Discovering the platform, and the problem

I appreciated Dan Weinreb's comment on why MIT switched from Scheme to Python in teaching freshman programming:

[C]omputer engineering was [once] based on starting with clearly-defined things (primitives or small programs) and using them to build larger things that ended up being clearly-defined. Composition of these fragments was the name of the game. However, nowadays, a real engineer is given a big software library, with a 300-page manual that’s full of errors. He’s also given a robot, whose exact behavior is extremely hard to characterize (what happens when a wheel slips?). The engineer must learn to perform scientific experiments to find out how the software and hardware actually work, at least enough to accomplish the job at hand.

It's very perceptive, this idea that a programmer's job is, in part, to work out how an existing system really behaves--through "scientific experiments." It flies in the face of the mathematical approach to programming, where we compose perfect units to accomplish our well-defined goal.

What's more, many programming tasks don't have a clearly-defined goal. You might instead want to explore a certain area by slapping things together. Or the goal may shift as you discover the problem terrain through coding.

None of this is incompatible with functional languages--but it might suggest an argument against purity: it might be very valuable for a language to have back doors, which allow you to do something improper, at least for a time, while you're poking around at the system or the domain.

But "back doors" are also an argument in favor of orthogonal language design. Being able to put functions anywhere, for example, allows you to code routines that are extremely unreadable--truly a vice. But when you're bodging around, it's very handy to be able to plop in a function where there once was a value, or the like. Functional languages win here, since they nearly always take orthogonality much further than imperative ones. (By this measure, though, Perl rates as a functional language!) Hopefully, after discovering what you needed to discover, you'll rewrite the code in a clear way, without very deep nesting of constructs, for example.