Let me give an example, I hope it’s not too complicated nor too application specific to understand for the non-lisper. The example is found in a Clojure project which I’m converting to Scala as a research project. I have introduced a macro named typecase
. The docs can be found here. As operands typecase
takes an expression to be evaluated a maximum of once (or never in case it can be optimized away), and a series of pairs (even number of arguments) of the form type-designator, expression (with no delimiting characters other than spaces), ending with an optional default value.
The type designators are not valid evaluatable expressions, rather just s-expressions (raw parsed but not yet compiled data) which the macro code examines; however the other expressions, (+ 1 2 3)
, 42
, 43
, 44
, and 45
, are valid, compilable expressions in the Clojure language. They are both available to the macro programmer, because parenthesisized lists are read by the parser as lists of lists of lists of symbols, strings, and numbers. This is treated by the macro as raw data, but it is the exact same data structure which is otherwise sent to the compiler–homoiconicity.
(typecase (+ 1 2 3)
(or String Double) 42
(and (satisfies int?) (satisfies odd?)) 43
(and (satisfies int?) (not (satisfies odd?)) (not (satisfies neg?))) 44
45)
The clojure language does not know what to do with (or String Double)
, (and (satisfies int?) (satisfies odd?))
, nor (and (satisfies int?) (not (satisfies odd?)) (not (satisfies neg?)))
, but the macro understands their expected syntax, and examines them to output another piece of code, which is either valid clojure code or another macro call which the compiler then expands.
When the macro looks at code like (and (satisfies int?) (satisfies odd?))
it understands it as the intersection of two type checks, and tries to figure out whether one type is a subtype of the other, or whether the types are disjoint, or whether a previous type check higher up in the type case has an effect on whether this type check can fail or must pass. Furthermore, when it sees (satisfies int?)
it interprets as the set of all values which when passed to the function named int?
would return true. Thus it looks up the function int?
, decompiles it to try to figure out whether any of the type checks in that function relate to other types being checked at this point of the typecase
.
The macro basically expands to an if then else, the then
part assumes the object has type Double
and the else
part assumes does not have type Double
, and reduces the logic of the remaining type checks two different ways for the two halves of the if
.
Since each half of the if
both contain a call to the typecase
macro, the compiler happily expands them during the process. Note also that the macro expansion outputs the macro calls which themselves include other type designators which the Clojure compiler will not understand as AST, but rather the recursive macro call will reduce, eventually to code which Clojure can convert to its AST.
Also note that this kind of macro, even if non-trivial, it not considered exotic. (OK, I admit the it is exotic that the macro decompiles the predicate like int?
) It is one of the normal kinds of things that Lisp macros do.
(let [value__20493__auto__ (+ 1 2 3)]
(if (sut/optimized-typep value__20493__auto__ Double)
(sut/typecase
value__20493__auto__
:sigma 42
:empty-set 43
:empty-set 44
:sigma 45)
(sut/typecase
value__20493__auto__
String 42
(and (or Long Integer Short Byte) (satisfies odd?))
43
(and
(or Long Integer Short Byte)
(not (satisfies odd?))
(not (satisfies neg?)))
44
:sigma 45)))
After all the macros are expanded, the final code looks like the following. That code is achieved by fine interaction between the compiler and the macro facility, a macro being a user hook into the compiler.
(let [v1 (+ 1 2 3)]
(if (gns/typep v1 'Double)
42
(if (gns/typep v1 'String)
42
(if (gns/typep v1 'Byte)
(if (odd? v1)
43
(if (neg? v1)
45
44))
(if (gns/typep v1 'Short)
(if (odd? v1)
43
(if (neg? v1)
45
44))
(if (gns/typep v1 'Integer)
(if (odd? v1)
43
(if (neg? v1)
45
44))
(if (odd? v1)
(if (gns/typep v1 'Long)
43
45)
(if (gns/typep v1 '(and Long (not (satisfies neg?))))
44
45))))))))