I came up with a derive
macro that specializes match
as a refactoring tool for self-hosted programs. I built it on top of blossoms
I think it's fun to use because any position in expression context can reflect on its role in another expression, using all values available at runtime. Source code to be published in new Denxi edition I hope to have out by the end of the month.
For now, I'll just go over the current syntax and behavior of the macro.
(module+ test
(derive derivative
#:as (declassify derivative)
#:taxa rule-position
#:diff
[`(derive derivative ,_ ... #:diff [,datum ,_] ,_ ...)
(rule-position datum)]
[(list-no-order (? rule-position? rp) _ ...)
rp]))
A derive
call expresses all the s-exp level edits you'd like to make to the surrounding program if your text editor's cursor sat at the corresponding call site. This specific example is kind of neat because the first match
pattern under #:diff
is targeting itself. The result of the expression is the datum form of that pattern.
(quote (quasiquote (derive derivative
(unquote _) ...
#:diff
[(unquote datum) (unquote _)]
(unquote _) ...)))
In general, (derive ...)
is a copy of the entire surrounding source code as a list, refactored using a front-end to match
. The match clauses under #:diff
control what code is replaced with new values. The clauses match elements provided in child-to-parent order, such that the (derive ...)
expression itself is the first expression used in match
. Next, it's the (module+ test ...)
, then any hypothetical traversal of proper or improper lists leading to the topmost enclosing S-expression. If no clauses match for an element at some level, then that element stays. All uses of match
occur during runtime. The derive
macro only serves to associate the call site with the runtime behavior.
Every element of an (im)proper list has its own child-to-parent traversal, so it is possible to distinguish equal?
values (like copies of module
forms) based on these traversals. This is a nice property, because it means that any lexically-equal derive
starts operating exactly on itself. This bleeds over into other benefits, like module
forms containing programs that can recognize their position in the whole module tree.
Finally, the derivative
, #:as (declassify derivative)
, and #:taxa
are all related. The first derivative
identifier binds the result of the final use of match
. The expression after #:as
uses that binding to compute the value of the whole derive
macro, kind of like how #:result
works in for/fold
. The declassify
function is an accessor that returns the value encapsulated by the ad-hoc #:taxa
(we don't want a rule-position
classifier, we want the classified value). The #:taxa
identifiers help generate temporary constructors and predicates that classify values without ambiguity in match patterns.
This example works by having a match
pattern extract the datum form of itself to classify accordingly. Since I'm traversing towards parent values, the list-no-order
rule naturally propagates classified elements. #:as
returns the element itself, without the classifying wrapper. In effect, this replaces the whole program's source code with just that one match
pattern.