A macro between quote and syntax-quote

I often find myself wishing for a reader macro that is between quote ' and syntax-quote `: I want to use unquote ~ and unquote-splicing ~@ but I do not want to get fully-qualified symbols. Let’s have an example.

;; What I want:
user=> (let [a 1] (magic-quote (let [x 0] (+ x ~a))))
(let [x 0] (+ x 1))

;; What the usual suspects do:
user=> (let [a 1] `(let [x 0] (+ x ~a)))
(clojure.core/let [user/x 0] (clojure.core/+ user/x 1))
user=> (let [a 1] '(let [x 0] (+ x ~a)))
(let [x 0] (+ x (clojure.core/unquote a)))

It seems to me that this magic-quote macro would be possible to implement but tricky. Does anyone know of an implementation, or maybe I am overthinking it and there is some easy way to do it?

1 Like

I assume you already know about this?

(let [a 1] `(let [x# 0] (+ x# ~a)))

Yes, sure; auto-gensym is great for macros but it’s not what I’m looking for. I’m not actually trying to generate Clojure code here, it was just the first example I had on my mind.

maybe template from https://github.com/brandonbloom/backtick?

7 Likes

Thanks, template is exactly what I’m looking for.

1 Like

Nice find! I might also use it in the future.

You can always force things by going through a string with (-> thing str symbol)

Like,

$ (let [a 1]
    (->> `(let [x 0] (+ x ~a))
      (w/postwalk
        #(if (symbol? %)
           (-> % name symbol)
           %))))
=> (let [x 0] (+ x 1))

name returns a string and we coerce it to a symbol.

1 Like

Oh, that’s pretty simple actually. There’s the caveat that if you want to have a fully-qualified symbol somewhere, it’s going to get unqualified, but that’s not always an issue.

Yeah, that’s a little more complicated… One way I can think of is by sending in a quoted form (not syntax quoted) and edit that… and just handle clojure.core/unquote when you get to it. And then grabbing the unquoted symbol from the locals using &env.

(defmacro locals []
  (into {}
    (map
      (fn [x#]
        [`'~x# x#])
      (keys &env))))

(defmacro magic-quote [args]
  `(let [ls# (locals)]
    (w/postwalk
       (fn [arg#]
         (if (and (seq? arg#) (= 'clojure.core/unquote (first arg#)))
           (get ls# (last arg#))
           arg#))
       (quote ~args))))

So then you can do

$ (def y 1)
=> #'user/y
$ (let [a 1] (magic-quote (let [x 0] (+ x user/y ~a))))
=> (let [x 0] (+ x user/y 1))

You’d have to switch up the locals handling for it to work in ClojureScript.

For posterity,

#mq/'

between a quote (') and syntax-quote (`)

For when you want to use unquote ~ but you do not want to get fully-qualified symbols. (unquote splicing ~@ not handled here)

In a cljc file, do:

;; src/mq/core.cljc
(ns mq.core
  (:require [clojure.walk :as w]))

(defmacro locals []
  (into {}
    (map
      (fn [x#]
        [`'~x# x#])
      (keys #?(:clj (if (not (:ns &env))
                      &env               ;; <- in clj
                      (:locals &env))    ;; <- in cljs
               :cljs (:locals &env)))))) ;; <- for self-hosted cljs (not tested)

(defmacro magic-quote [args]
  `(let [ls# (locals)
         *gs*# (atom {})]
    (w/postwalk
       (fn [arg#]
         (if (and (seq? arg#) (= 'clojure.core/unquote (first arg#)))
           (get ls# (last arg#))
           (if (-> arg# str last (= \#))
             (let [sym# (->> arg# str drop-last (apply str) symbol)
                   gs# (-> (gensym)
                         (str "__auto__") rest (->> (apply str sym#) symbol))]
               (or (-> @*gs*# (get sym#))
                 (-> *gs*# (swap! assoc sym# gs#) (get sym#))))
             arg#)))
       (quote ~args))))

(defn mq [args]
  `(magic-quote ~args))

Then, in a cljs file:

;; src/mq/core.cljs
(ns mq.core
  (:require-macros [mq.core]))

And in a data readers cljc file:

;; src/data_readers.cljc
{mq/'   mq.core/mq}

Then we can do like this in Clojure:

$ clj
Clojure 1.10.1
user=> (require 'mq.core :reload)
nil
user=> (let [a 1] #mq/'(let [x 0] (+ x ~a)))
(let [x 0] (+ x 1))

Or from ClojureScript:

$ clj -m cljs.main -c mq.core -re node -r
ClojureScript 1.10.520
cljs.user=> (let [a 1] #mq/'(let [x 0] (+ x ~a)))
(let [x 0] (+ x 1))
;; and we can do gensymed locals too
cljs.user=> (let [a 1] #mq/'(let [x# 0] (+ x# ~a)))
(let [x__9__auto__ 0] (+ x__9__auto__ 1))

Wondering… what would you want to use it for?

2 Likes

I may be misunderstanding, but are you looking for ~'?

(ns scratch.symbols)

;; What about ~'?

(do
  `(qualified ~'unqualified))
;; => (scratch.symbols/qualified unqualified)

Its use isn’t exactly as you asked for, but finding ~' helped me in a similar situation.

1 Like

@teodorlu yeah, that works. But Miikka was looking for a thing where literally unqualified symbols don’t automatically get expanded to qualified symbols.

backtick is definitely the tool to use for the closest semantics to Clojure’s syntax unquote. As a tagged literals, #mq/' ends up applying its gensyms across nesting levels, so you end up with a pretty different templating gensym semantic.

If you want to try #mq/' from a git dep, here it is. Let me know if you find this method useful for anything.

1 Like

Oh, I see. So you would want "syntax quote with ability to insert stuff but without resolving namespaces. And I have to admit that I hadn’t considered gensyms or “nested use”.

Thanks for the explaination!

I’ve wished for this many times, but can’t remember why. This time it was because I wanted to generate some Z3 code. It conveniently has Lisp syntax. Here’s my code using backtick. Not amazing, but a bit nicer than generating strings.

1 Like

@Miikka Via @alexmiller on Clojurians Slack I learned about clojure.template:

(require '[clojure.template :as t])
(t/apply-template '[a] '(let [x 0] (+ x a)) '[1])
(let [x 0] (+ x 1))
7 Likes

Clojure core just never stops to amaze me!

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.