This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-08-03
Channels
- # announcements (5)
- # babashka (7)
- # beginners (119)
- # biff (4)
- # cider (7)
- # clj-kondo (26)
- # cljfx (3)
- # cljs-dev (2)
- # clojure (28)
- # clojure-austin (18)
- # clojure-europe (9)
- # clojure-france (6)
- # clojure-norway (4)
- # clojure-uk (3)
- # clojurescript (6)
- # community-development (1)
- # core-async (4)
- # cursive (9)
- # data-science (12)
- # datomic (13)
- # duct (18)
- # emacs (15)
- # etaoin (5)
- # events (13)
- # honeysql (46)
- # hyperfiddle (9)
- # jackdaw (5)
- # jobs (13)
- # keechma (4)
- # lsp (37)
- # malli (32)
- # nbb (14)
- # off-topic (10)
- # other-languages (2)
- # polylith (4)
- # programming-beginners (3)
- # reagent (27)
- # reitit (1)
- # shadow-cljs (32)
- # sql (11)
- # tools-build (5)
- # tools-deps (3)
- # vim (14)
- # xtdb (11)
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?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
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
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
@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))
so for macros to produce code all the info should be available at compile time
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@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
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)
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
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"
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)))
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(let [local-bindings '{project-id 100}]
(expand-locals local-bindings project-id))
like that ... because the locals are more often only known at runtimeYeah, 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.Without that, the values are "printed" into the let bindings and it doesn't work with arbitrary objects.
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)))