Maybe there’s a more precise way to define homoiconicity, but frankly I think it misses the point. What makes Lisp’s syntax powerful is not the fact that it can be represented as a data structure, it’s that it’s possible to read it without parsing.
It’s hard to explain these concepts with traditional terminology, because the distinction between reading and parsing simply doesn’t exist for languages without macros.
Parsing vs reading: the compiler’s view
In almost every non-Lispy language ever, the front end of every interpreter and compiler looks pretty much the same:
Take the text, run it through a parser, and you get out an AST. But that’s not how it works when you have macros. You simply can’t produce an AST without expanding macros first. So the front-end of a Lispy language usually looks more like:
What’s this intermediate syntax tree? It’s an almost entirely superficial understanding of your program: it basically does paren-matching to create a tree representing the surface nesting structure of the text. This is nowhere near an AST, but it’s just enough for the macro expansion system to do its job.
Parsing vs reading: the macro expander’s view
1 2 3
you know for sure that it’s a ForInStatement, as defined by the spec (I’m using
for as a macro. When the macro expander encounters:
it knows nothing about the contents of the expression. All it knows is the macro definition of
for. But that’s all it needs to know! The expander just takes the two subtrees,
(key obj) and
(print key), and passes them as arguments to the
Parsing vs reading: the macro’s view
Here’s a simple
for macro, written in Racket:
This macro works by pattern matching: it expects two sub-trees, the first of which can itself be broken down into two identifier nodes
e1, and it expands into the
for-each expression. So when the expander calls the macro with the above example, the result of expansion is:
The power of the parenthesis
How many arguments does
None of this is to say that it’s impossible to design a macro system for languages with non-Lispy syntax. My point is just that the power of Lisp’s (Scheme’s, Racket’s, Clojure’s, …) macros comes not from being somehow tied to a central data structure of the language, but rather to the expander’s ability to break up a macro call into its separate arguments and then let the macro do all the work of parsing those arguments. In other words, homoiconicity isn’t the point,