Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure, unquote slicing outside of syntax quote

In clojure, we can use unquote slicing ~@ to spread the list. For example

(macroexpand `(+ ~@'(1 2 3))) 

expands to

(clojure.core/+ 1 2 3)

This is useful feature in macros when rearranging the syntax. But is it possible to use unquote slicing or familiar technique outside of macro and without eval?

Here is the solution with eval

(eval `(+ ~@'(1 2 3))) ;-> 6

But I would rather do

(+ ~@'(1 2 3))

Which unfortunately throws an error

IllegalStateException Attempting to call unbound fn: #'clojure.core/unquote-splicing  clojure.lang.Var$Unbound.throwArity (Var.java:43)

At first I thought apply would do it, and it is indeed the case with functions

(apply + '(1 2 3)) ; -> 6

However, this is not the case with macros or special forms. It's obvious with macros, as it's expanded before apply and must be first element in the form anyway. With special forms it's not so obvious though, but still makes sense as they aren't first class citizens as functions are. For example the following throws an error

(apply do ['(println "hello") '(println "world")]) ;-> error

Is the only way to "apply" list to special form at runtime to use unquote slicing and eval?

like image 216
Tuomas Toivonen Avatar asked Feb 05 '23 10:02

Tuomas Toivonen


1 Answers

Clojure has a simple model of how programs are loaded and executed. Slightly simplified, it goes something like this:

  1. some source code is read from a text stream by the reader;

  2. this is passed to the compiler one form at a time;

  3. the compiler expands any macros it encounters;

  4. for non-macros, the compiler applies various simple evaluation rules (special rules for special forms, literals evaluate to themselves, function calls are compiled as such etc.);

  5. the compiled code is evaluated and possibly changes the compilation environment used by the following forms.

Syntax quote is a reader feature. It is replaced at read time by code that emits list structure:

;; note the ' at the start
user=> '`(+ ~@'(1 2 3))
(clojure.core/seq
  (clojure.core/concat (clojure.core/list (quote clojure.core/+)) (quote (1 2 3))))

It is only in the context of syntax-quoted blocks that the reader affords ~ and ~@ this special handling, and syntax-quoted blocks always produce forms that may call a handful of seq-building functions from clojure.core and are otherwise composed from quoted data.

This all happens as part of step 1 from the list above. So for syntax-quote to be useful as an apply-like mechanism, you'd need it to produce code in the right shape at that point in the process that would then look like the desired "apply result" in subsequent steps. As explained above, syntax-quote always produces code that creates list structure, and in particular it never returns unquoted expressions that look like unquoted dos or ifs etc., so that's impossible.

This isn't a problem, since the code transformations that are reasonable given the above execution model can be implemented using macros.

Incidentally, the macroexpand call is actually superfluous in your example, as the syntax-quoted form already is the same as its macroexpansion (as it should be, since + is not a macro):

user=> `(+ ~@'(1 2 3))
(clojure.core/+ 1 2 3)
like image 74
Michał Marczyk Avatar answered Feb 14 '23 09:02

Michał Marczyk