@alexyakushev I'm reading the scoped value javadoc and I'm not sure it requires a new thread https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/ScopedValue.html
ScopedValue.where(NAME, "duke1").run(() -> doSomething());
Isn't this equivalent to our binding macro?
It's bound in the context of the thread but no one else reads itYeah, I must have misremembered.
Not a new thread but it is separate method call, whereas Var.push/popBindings uses try/finally
Not saying that's a problem, but this is a difference
It has many similarities
As dynamic values are what explicitly inspired this design it is not surprising
Looks like there's no way to provide an initial value, so the initial code would need the ScopedValue and the initial value, and then dereferences would need to use DYNVAR.orElse(DYNVAR_INIT) everywhere, instead of DYNVAR.get() if I'm reading that right?
Oh, .orElse() does not accept null so you'd need to use a conditional on .isBound() instead for that case...
Still would be faster than dynvar lookup, no? And let's not forget the cost of the binding macro (very large!)
The first lookup via .get() has to do a chained lookup to find the nearest bound value (but then it caches it) but, probably faster even in the single use case.
I might try it out at work, just to see how it "feels" in Clojure code...
With a small macro to wrap it the differences are hard to spot
more importantly, these are designed to work well with vthreads
(as opposed to ThreadLocal)
Tried it out in some code at work:
(def ^:private ^:dynamic *sql-add-ons* nil)
becomes
(def ^:private sql-add-ons (ScopedValue/newInstance))
And
(binding [*sql-add-ons* (atom {})]
becomes
(-> sql-add-ons ;; previously: binding [*sql-add-ons* (atom {})]
(ScopedValue/where (atom {}))
(ScopedValue$Carrier/.call
(fn []
and each reference changes from *sql-add-ons* to (ScopedValue/.get sql-add-ons)(safe since it's never used unbound, or assuming that initial value)
Probably more pleasant with
(defmacro where1 [[nom v] & body] `(let [nom# ~nom] (.run (ScopedValue/where nom# ~v) (fn* [] (do ~@body)))))
(where1 [name "duke"] (println (.get name)))It needs to be .call if you want a result back.
you can draw the rest of the owl 😄
But, yeah, a little bit of syntactic sugar makes it look a lot more like binding... although it isn't too bad in raw from, TBH.
I also didn't handle multiple bindings in one call
🟤 🟤 🦉 Easy!
Owl!
(defmacro where
[bindings & body]
(assert (even? (count bindings)))
(let [pairs (partitionv 2 bindings)]
(doseq [[sym] pairs] (assert (symbol? sym)))
(let [[[p0 v0] & pairs] pairs
form (reduce (fn [carrier [p v]]
`(ScopedValue$Carrier/.where ~carrier ~p ~v))
`(ScopedValue/where ~p0 ~v0)
pairs)]
`(ScopedValue$Carrier/.call
~form
(fn* [] (do ~@body))))))
(def x (ScopedValue/newInstance))
(def y (ScopedValue/newInstance))
(where [x "Duke"
y "Java"]
(String/.toUpperCase (str (.get y) (.get x))))
Now I wish Java had an interface Box<T> that declares get() -> T
Isn't that just Supplier?
i would expect a Box<T> to also give a set
I just want to be able to use deref on ScopedValue
Multiple bindings:
ScopedValue.where(k1, v1).where(k2, v2).run(() -> ... );