|
I'm not sure what the relationship between fexprs and homoiconicity is supposed to be. If anything, the arguments to an fexpr in a Lisp with lexical scoping are opaque and cannot be manipulated, since they are wrapped up in a closure by the interpreter. Plus, not every macro can be written as an fexpr (for example, LOOP). In contrast, fexprs seem to work well in PicoLisp because it uses dynamic scoping exclusively. Traditional Lisps are only functional in the first-class-function sense, but not in the mathematical sense where functions give the same output for the same inputs. Furthermore, they use an eager reduction rule for function applications, and not lazy reduction. Put together, programmers rely on knowing the implicit control flow so that effects occur in their proper order. All fexprs really do is let the programmer define constructs that manipulate this control flow in non-standard ways. If you think about it: why have fexprs be part of the language if all you need to do is wrap each argument in (lambda () ...)?[1] (If you make { and } be a reader macro for (lambda () (progn ...)), then you would possibly save a little typing.) (In Haskell, the eager evaluation is sort of abstracted into the IO monad, and pervasive lazy evaluation is that everything is an fexpr.) I think of Lisps (Common Lisp in particular) as actually being two languages joined together: the metaprogramming (macro) language and the core Lisp language. It can be a bit confusing distinguishing them because of how metaprogramming stages are interleaved and how the metaprograms are written in Lisp itself. The macro expansion language is a simple tree rewrite language, where for each node, the system looks to see whether there is a corresponding rewrite rule (a macro), then replaces that subtree with that rule's result. This continues until quiescence, at which point the completed program is passed to the core Lisp interpreter/compiler. One strange Lisp variant is Mathematica, where the entire language is based around these tree rewrite rules --- it's all basically macro expansion. In principle, this means Mathematica does not need macros, but in practice it can be very difficult to control the order of the rewrites. Lisp macros are first class in a certain way, at least in that you can refer to them with (macro-function ...). I know what you mean though, how you can't just apply #'and to a list of booleans and get the actual boolean "and." I sort of think that Common Lisp should have distinguished between the function and macro slots of a symbol. That way you could define both a macro and function version of a symbol if they both make sense. This would also better distinguish the metaprogramming and evaluation stages. Something I wish were true about Lisp is that eval(eval(x)) == eval(x) (at least if the expression x has no side effects!). There are a few things in the design that make this difficult or impossible: (1) evaluating a symbol gets its lexical or global binding, so double evaluation of 's will give the value of s and (2) there is no distinction between a form and a list literal, other than through quotation. A way around (1) is to use MDL-style explicit accessors to get the bound value of a symbol and make symbols self-evaluating (so .x gets the lexical value of the symbol x), and a way around (2) is to have tagged lists like in MDL, where the tag is part of the pointer to the first cons cell. Mathematica does this too, to some extent. For example, {1,2,1+2} in Mathematica is short for List[1,2,Plus[1,2]], which evaluates to List[1,2,3]. "List" is not an element of the list, but rather the tag (called the "head"). (There's a hole in my proposal, however: if the lexical value of a symbol is a form, then double evaluation will evaluate to what that form represents. I don't know away around this. This might simply be a formal logical contradiction from trying to collapse different levels of representation into one, a Russell-like paradox.) I'll just throw something else I've been thinking about here: the old Lisp-1 versus Lisp-2 debate is about whether a symbol should refer to a single thing, or whether using it as a function vs using it as a value should refer to different things. The utility of two namespaces seems to be than when Lisps were dynamically scoped, setting the value of a symbol wouldn't cause a clash if that symbol happened to be used as a function elsewhere. However, with the advent of lexical scoping in Lisps in the 80s this became a bit less of an issue, though it remained in the design to avoid collisions in macro expansion. I wonder if the "correct" Lisp-2 ought to be global vs local bindings for a symbol, at the cost of needing to annotate symbols like .x and ,x for local and global bindings, like in MDL. As a convenience, (f .x) would be short for (,f .x). [1] This is the paper in which Kent Pitman argued against fexprs in Lisp: https://www.nhplace.com/kent/Papers/Special-Forms.html |
I wouldn't call Mathematica a Lisp, given that it is a runtime rewrite system and not based on a defined evaluator (which also makes Lisp relatively easy to compile and also compilable to relatively efficient code).
> That way you could define both a macro and function version of a symbol if they both make sense.
There is a variant of that in CL called 'compiler macro'.
> The utility of two namespaces seems to be than when Lisps were dynamically scoped
One still has the same problems with global symbols in Scheme.
Here the parameter variable LIST shadows the global function LIST.