Fork me on GitHub
#clojure
<
2022-08-03
>
jumar10:08:15

I'm struggling with writing a little macro that would make symbols within a map available as let bindings. Let's call it expand-locals First naive attempt doesn't work at all:

(defmacro expand-locals [bindings-map & body]
  `(let ~(vec (mapcat identity bindings-map))
     (do ~@body)))

(expand-locals {'project-id 100})
;;=>
   Call to clojure.core/let did not conform to spec.
   #:clojure.spec.alpha{:problems
                        ({:path [:bindings :form :local-symbol],
                          :pred clojure.core/simple-symbol?,
                          :val 'project-id,
...
When extracting the body of macro and trying manually, it seems to work:
(let [bindings-map {'project-id 100}]
  `(let ~(vec (mapcat identity bindings-map))
     (do ~'project-id)))
;; => (clojure.core/let [project-id 100] (do project-id))
I tried a few different approaches but it only got worse. How should I approach this problem?

Martin Půda11:08:07

Don't quote symbols in the hash-map (and don't use do in let):

(defmacro expand-locals [bindings-map & body]
  `(let ~(vec (mapcat identity bindings-map))
     ~@body))

(expand-locals {a 1 b 2} a)
=> 1
(expand-locals {a 1 b 2} b)
=> 2
(expand-locals {a 1 b 2} (+ a b))
=> 3

jumar11:08:36

Quoting was the problem in this case. however, it doesn't help beyond a very simplistic case, like when passing the input literally.

(defmacro expand-locals [bindings-map & body]
  `(let [~@(mapcat identity bindings-map)]
     ~@body))

;; this works
(expand-locals {project-id 100}
               project-id)
;; => 100

;; but this doesn't:
(def mylocals {'project-id 100})
(expand-locals my-locals
               project-id)
;;=> throws:    Don't know how to create ISeq from: clojure.lang.Symbol

jumar11:08:59

Obviously, I need to quote the symbol here because it's normally not available.

Martin Půda11:08:12

My only idea is this one, but eval is evil and there has to be something better:

(defmacro expand-locals [bindings-map & body]
  `(let ~(vec (mapcat identity (eval bindings-map)))
     ~@body))

(def my-locals {'project-id 100})

(expand-locals my-locals
               project-id)
=> 100

(expand-locals {'project-id 100}
               project-id)
=> 100

jumar11:08:13

Thanks. Yeah, I don't want to do that but better than nothing 🙂.

jpmonettas11:08:00

@U06BE1L6T I don't think that is possible, because to produce a let you will need all the information at compiletime. So things like this will not work :

(defn foo [my-locals]
  (expand-locals my-locals project-id))

jpmonettas11:08:40

so for macros to produce code all the info should be available at compile time

Ed11:08:42

Supporting a var isn't too hard:

(def my-bindings '{project-id 100})

(defmacro expand-locals [bindings-map & body]
  `(let ~(if (symbol? bindings-map)
           (deref (ns-resolve *ns* bindings-map))
           (vec (mapcat identity bindings-map)))
     ~@body))

(comment

  (macroexpand-1 '(expand-locals {project-id 100} project-id)) ;; => (clojure.core/let [project-id 100] project-id)
  (macroexpand-1 '(expand-locals my-bindings project-id))      ;; => (clojure.core/let {project-id 100} project-id)

  )
but looking up symbols in local scope is harder

👍 1
Ed11:08:48

maybe wrap each value in the map in a function call?

jumar11:08:27

@U01AKQSDJUX your example doesn't work for me:

(def my-bindings '{project-id 100})

(defmacro expand-locals [bindings-map & body]
  `(let ~(if (symbol? bindings-map)
           (deref (ns-resolve *ns* bindings-map))
           (vec (mapcat identity bindings-map)))
     ~@body))

(expand-locals my-bindings project-id)
;;=>
1. Caused by clojure.lang.ExceptionInfo
   Call to clojure.core/let did not conform to spec.
   #:clojure.spec.alpha{:problems
                        [{:path [:bindings],
                          :pred clojure.core/vector?,
                          :val {project-id 100},
                          :via
                          [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings],
                          :in [0]}],
                        :spec
                        #object[clojure.spec.alpha$regex_spec_impl$reify__2503 0x1852c88 "clojure.spec.alpha$regex_spec_impl$reify__2503@1852c88"],
                        :value ({project-id 100} project-id),
                        :args ({project-id 100} project-id)}
                 alpha.clj:  712  clojure.spec.alpha/macroexpand-check

jumar12:08:24

And trying the eval approach with more complicated example; it doesn't work when trying to capture a ring request where there's a reference to a connection pool:

1. Caused by java.lang.RuntimeException
   Can't embed object in code, maybe print-dup not defined: HikariDataSource (HikariPool-1)

jumar12:08:33

Ed, I think you forgot to make a binding vector for the var case - this works:

(defmacro expand-locals [bindings-map & body]
  `(let [~@(if (symbol? bindings-map)
             (mapcat identity (deref (ns-resolve *ns* bindings-map)))
             (mapcat identity bindings-map))]
     ~@body))

(expand-locals my-locals project-id)
;; => 100
(expand-locals {project-id 100}
               project-id)
;; => 100

jumar12:08:28

I guess I really have to lookup those values dynamically instead of placing the values inside let bindings. And I'm actually fine with requiring that bindings-map is a symbol denoting a var. I made something like this

(defmacro expand-locals [bindings-var-sym & body]
  (let [bindings (deref (ns-resolve *ns* bindings-var-sym))]
    (->> body
         (walk/postwalk
          (fn [form] (if (and (simple-symbol? form)
                              (contains? bindings form))
                       `(get ~bindings-var-sym '~form)
                       form)))
         (mapcat identity))))
(expand-locals my-locals
               (let [a 1]
                 (str "b/" project-id "/" a)))
;; => "b/100/1"

Ed13:08:36

apologies ... I got distracted from chatting on slack 😉

Ed13:08:23

yes ... you're right, the code I pasted above didn't work - sorry about that

jumar13:08:39

No problem, thanks for the idea and the code! I actually ended up mixing these two approaches: https://github.com/jumarko/clojure-experiments/pull/21

(defmacro expand-locals [bindings-var-sym & body]
  (let [bindings (deref (ns-resolve *ns* bindings-var-sym))]
    `(let [~@(mapcat (fn [[sym _]]
                       [sym `(get ~bindings-var-sym '~sym)])
                     bindings)]
       ~@body)))

Ed13:08:44

something like this

(defmacro expand-locals [bindings-map & body]
  (let [bindings (-> bindings-map
                     (cond-> (symbol? bindings-map) (some->> (ns-resolve *ns*) deref))
                     (->> (into [] (mapcat identity))))]
    `(let ~bindings
       ~@body)))
will look up the var

👍 1
Ed13:08:06

ah .. cool .. you got something sorted then?

Ed13:08:39

I don't think it's possible to look up arbitrary locals

Ed13:08:20

(let [local-bindings '{project-id 100}]
    (expand-locals local-bindings project-id))
like that ... because the locals are more often only known at runtime

jumar13:08:35

Yeah, this is the key piece to make it work in my setting:

`(get ~bindings-var-sym '~sym)
I realized I really care mostly about the case when the bindings are stored in a global var.

Ed13:08:30

fair enough ... remember that the var is only going to be derefed at macroexpansion time

jumar13:08:40

Without that, the values are "printed" into the let bindings and it doesn't work with arbitrary objects.

Ed13:08:55

so if you update the contents of the var, you'll have to reexpand all the macros

Ed13:08:16

you could also delay evaluation of the vals in the bindings map by wrapping them in a function

(defmacro expand-locals [bindings-map & body]
  (let [bindings (-> bindings-map
                     (cond-> (symbol? bindings-map) (some->> (ns-resolve *ns*) deref))
                     (->> (into [] (mapcat (fn [[n v]] [n (list (list 'fn [] v))])))))]
    `(let ~bindings
       ~@body)))

jumar13:08:29

This is mostly for debugging, so I'm basically calling calling this expand-locals manually anyway.