Hi everyone,
Micha Niskin and I wish to share that we identified concretely an
aspect of the Feature Macros proposal that we think makes the whole
thing unsound. We unannounce it :-)
We couldn't have reached this conclusion without the vigorous argument
and discussion the Clojure community was kind enough to indulge us
with. We invite everyone involved to share in the joy that comes with
working an idea completely through. Go us!
The problem is one of composition. While operations being applied
under the usual Lisp eval rules receive arguments after evaluation,
macros applied under the usual Lisp macro expansion rules *do not*
receive their arguments after macro-expansion. It is a curious
asymmetry.
Because macros have the option of expanding their arguments, and
because most don't, we can't pass macros code containing other macros
and expect the same kind of inside-out composition we get with normal
Lisp. Consider the normal Lisp function, which can be ignorant of the
code contributing to the argument values it sees:
(+ 1 2)
(+ (inc 0) (* 2 1)).
In both cases, + sees an arg list of [1 2]. It doesn't know or care
what code represented its arguments prior to evaluation.
Macros aren't like this. We can't pass arguments to macros that
contain macro calls and expect the top-most macro to be ignorant of
how its arguments came to be.
Consider the ns macro and the case-platform macro from our proposal
[1]. If ns macro-expanded its arguments, we could achieve FX-like
functionality like:
(ns example-portable-ns
(:require (case-platform
:clj clojure.core
:cljs cljs.core)))
But ns doesn't macroexpand its args, and never will.
The workaround, which we applied in ignorance of the asymmetry as the
ns+ macro in the proposal, is to wrap. If you wrap an existing macro,
you have an opportunity to control the expansion of its arguments.
This means that for every macro you want to exhibit the new semantic,
you need to wrap it. This results in a code explosion problem (in the
form of wrapper macros) which is the same problem we're trying to
solve.
The #+ and #- reader macros of Feature Expressions circumvent this
problem, because as reader constructs, they are the only
possibly-conditionalized thing preceding macro expand. The code
containing them cannot know that they exist, in the same way that a
macro which received its arguments expanded sees no macros. With
them, regular ns works fine, because it is ignorant of the
reader-level dispatch that preceded its expansion:
(ns example-portable-ns
(:require #+clj clojure.core #+cljs cljs.core))
We're still not super-enthusiastic about FX as it stands, because
we're terrified by the prospect of losing code generation forever. We
think there might alternatives to mitigate though, such as boxing
feature-read forms in a new special form with :+ and :- meta hung on
it. At least then we could generate and print things without
descending immediately into string munging.
We encourage you to think deeply and critically about FX too. We
tried to, and were rewarded by learning something awesome about Lisp.
Yes, FX was invented by geniuses in the beforetimes and is probably
good, but if we "cargo cult" without reasoning anew for ourselves why it's good, we
just might regret it.
We'd like to acknowledge Brandon Bloom, who imagined something similar
to the problem we describe on the Feature Expressions design page back
in 2013. [2] We thank also Colin Fleming, whose mention in IRC of a "fear of an explosion of
+ macros" caused us to see ns+ in a new and unsavory light.
Everything we mention was probably also known by somebody, but it was
hard to Google. If anyone knows any related references regarding the
weird missing macroexpand semantic, do send them our way.
Oh, and here is a prototype implementation of the Weird Semantic:
https://gist.github.com/alandipert/331885e36756e691f41aAlan Dipert
Micha Niskin
1.
https://github.com/feature-macros/clojurescript/tree/feature-macros/feature-macros-demo2.
http://dev.clojure.org/display/design/Feature+Expressions?focusedCommentId=6390065#comment-6390065