java 2026-10-20

@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 it

Yeah, 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

Alex Miller (Clojure team) 2026-10-20T16:45:18.482929Z

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

Alex Miller (Clojure team) 2026-10-20T18:06:17.792899Z

more importantly, these are designed to work well with vthreads

💯 1
Alex Miller (Clojure team) 2026-10-20T18:06:23.308759Z

(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))))

💪🏻 1

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

thats on us for IDeref not being a protocol

➕ 1

Multiple bindings:

ScopedValue.where(k1, v1).where(k2, v2).run(() -> ... );