Parentheses are Just Typechecking
Consider this Scheme expression, taken from the Rosetta Code example for the Mandelbrot set:
(boolean->integer
(inside?
(make-rectangular (+ x-offset (* pixel-size i))
(- y-offset (* pixel-size j))))
How would we translate this to a stack language, like Forth? Stack languages are a mirror image of Lisps, written in postfix rather than prefix notation. So a Forth-like version of this expression would be the same words in reverse, without the parentheses:
j pixel-size * y-offset -
i pixel-size * x-offset +
make-rectangular inside? boolean->integer
But why does this necessarily have to be written postfix? A Forth that evaluates right-to-left would look almost identical to a prefix notation language, just with no delimiters.
boolean->integer
inside?
make-rectangular + x-offset * pixel-size i
- y-offset * pixel-size j
It's not exactly more readable than the original. But if you reverse the words and evaluate it as if it's Forth, the expression is the same, with or without the parens.
Here's one way to look at this: for simple expressions, parentheses are a form
of static typechecking. If you model a Lisp as a stack language that evaluates
from right to left, each parenthesized block must push exactly one value onto
the stack, and must not consume any values. In Forth terms, each ( ... )
enforces that its contents are of the type -- a
.
This equivalence makes some assumptions. For one, it's assuming
a Lisp-2 (even though Scheme is
a Lisp-1), since a closure returned from a stack-language function would just be
pushed onto the stack, so ((compose a b) c)
wouldn't work.
I'm also only talking about simple expressions here, ignoring the other more
significant differences between Lisps and Forths, like lists and variadic
functions (which would still need parentheses), as well as variable definitions
versus stack munging operators like dup
or swap
.
Even with these caveats, the equivalence is still fascinating. And it makes me aware of some gaps in the design space of stack languages and Lisps:
Why have I never seen...
-
...a stack language that evaluates right-to-left? Non-stack-based languages evaluate right-to-left within individual expressions, so this could be surprisingly intuitive. Yes, we often expect function arguments to evaluate left-to-right, but some mainstream languages (notably C) leave argument evaluation order undefined.
-
...a stack language that allows parentheses as a sanity check?
-- a
expressions are common, and allowing the programmer to wrap them in parentheses to enforce this type could make stack code much more readable. (I haven't actually used many stack languages, so this probably exists, I just haven't seen it.) -
...a Lisp that can drop nested parentheses when the meaning is clear from context? This one takes some explanation.
Consider a Lisp-2 where functions are written in
UpperCamelCase
to distinguish them from values inlower-kebab-case
. Then an expression like(Foo bar (Baz qux))
could be rewritten as(Foo bar Baz qux)
.The simplest version of this assumes each argument goes to the first function to its left. That's already useful for saving parentheses, but it's not the reverse-Forth syntax I've been talking about.
If we don't make this simplifying assumption, then expressions like
(A B C)
are ambiguous. This would be(A (B) (C))
ifB
andC
take 0 arguments andA
takes 2, but(A (B (C)))
ifA
andB
both take 1 argument. Fixing this ambiguity requires a static type system. We could use a stack instead, but then it's not really a Lisp anymore.(Or is it?)