Today I wanted to look into Clojure’s macro functionality and I thought recreating Picolisp’s make environment would be a fairly easy exercise.
At first I tried to accomplish everything with the help of generated symbols which are sometimes used in macros to generate containers whose names are unique and will therefore not conflict with other names at run time.
This was a dead end, no matter how I tried I could not get the symbol generated in the make macro to be accessible in the link function/macro.
In the end I opted for a dynamic var, with a code that looks like this:
(declare ^:dynamic mlst) (defn made [sq] (set! mlst sq)) (defn yoke [& args] (doseq [el args] (set! mlst (cons el mlst)))) (defn chain [& args] (set! mlst (into mlst (flatten args)))) (defn link [& args] (set! mlst (into mlst args))) (defmacro make [& body] `(binding [mlst ] ~@body (vec mlst)))
So the make macro takes any number of non-evaluated forms which are turned into a list (thanks to the & in the argument vector) which is spliced (that’s what the ~@ does, ie ((bla bla) (foo bar)) -> (bla bla) (foo bar) ) into a quoted (note the backtick) binding call. This binding call will later be run in place of the macro at run time.
Note also that since we’re using a binding call we should not run into problems with other threads overwriting the data in mlst, we should be “thread safe”, or “thread local”. At least as far as I’ve been able to tell, don’t take my word for it I’m still a noob.
Try this macro expansion and you will see what I mean above:
(macroexpand-1 '(make (doseq [el '(1 2 3)] (link el)) (yoke "put this first")))
You get this:
(clojure.core/binding [casino.models.game/mlst ] (doseq [el (quote (1 2 3))] (link el)) (yoke "put this first") (clojure.core/vec casino.models.game/mlst))
Here is some completely pointless code that demonstrates the full functionality (due to the bind call nested makes are not a problem):
(println (make (doseq [el [1 2 3 4]] (if (even? el) (chain (list "even" el)) (link "odd" el "some other odds:" (make (link "this will get replaced") (made ) (doseq [el [13 15 17]] (link "odd:" el)))))) (yoke "and this second" "this should be first")))
The above will result in: [this should be first and this second odd 1 some other odds: [odd: 13 odd: 15 odd: 17] even 2 odd 3 some other odds: [odd: 13 odd: 15 odd: 17] even 4]
(println (take-last 10 (make (doseq [el (take 1000000 (range))] (when (or (even? el) (= 0 (mod el 5))) (link (str (if (even? el) "even " "odd ") "number: " el)))))))
The above takes roughly 3 seconds so it’s not the fastest stuff in the world…
The proper way of doing things is for instance with a for call. The below code takes roughly one second to run:
(println (take-last 10 (for [el (take 1000000 (range)) :let [res (str (if (even? el) "even " "odd ") "number: " el)] :when (or (even? el) (= 0 (mod el 5)))] res)))
However, coming from Picolisp, I think you know which form I think looks prettier (especially if you do too)…