A tiny (~100 LOC) Clojure library for leveraging java.lang.ScopedValue.
It introduces 4 macros, and 4 functions.
defscoped: declares a scoped Var (analogous to declaring a :dynamic Var)scoping: analogous to clojure.core/bindingwith-scope: analogous to clojure.core/with-bindingsscoped-fn*: analogous to clojure.core/bound-fn*scoped-fn: analogous to clojure.core/bound-fnget-thread-scope: analogous to clojure.core/get-thread-bindingsscoped-future-call: analogous to clojure.core/future-callscoped-future: analogous to clojure.core/futureWithout getting into too much detail, ScopedValue is essentially a better ThreadLocal,
especially in-lieu of virtual-threads. See the JEP.
I like to name such 'global' vars with upper-case, to make them stand out visually
(similarly to how dynamic vars must start/end with *). So let's declare a couple:
(defscoped NAME "Some doc-string")
(defscoped LANG "Some other doc-string")
With these in place, we can now scope them:
(scoping [NAME "duke"
LANG "java"]
(do-something!))
where do-something! can be anything that (presumably) reads the scoped-var(s):
(defn do-something! []
(println "Name is" @NAME)
(println "Lang is" @LANG)
:done)
The important thing here is the use of @ (i.e. deref), which is the equivalent of ScopedValue::get in Java.
Unlike ThreadLocal bindings, ScopedValue ones are not auto-conveyed to future/agent threads.
You can however, capture the current scope with get-thread-scope, and manually reuse/propagate it with with-scope.
That's what scoped-fn* essentially does. Alternatively, a scoped-future helper macro is provided.
Re-scoping (i.e. nested scoping) is fully supported, just like re-binding (via binding).
Unfortunately, when declaring scoped-values via defscoped, what you get is actually a wrapper type
(i.e. DerefableScopedValue), which implements clojure.lang.IDeref. You see, it's kind of impossible to have
a raw java.lang.ScopedValue, or a proxy of it, because:
clojure.lang.IDeref is not a protocol, so it cannot be extendedjava.lang.ScopedValue is a final class, so it cannot be inherited fromTherefore, until Clojure adds native deref support (see CLJ-2926),
we have to deal with some kind of wrapper type, and that presents a challenge, because now, code that deals with instances
created via defscoped VS native ones, has to look different! More specifically, you cannot call deref on a native
java.lang.ScopedValue - you have to manually call .get on it.
Copyright © 2025 Dimitrios Piliouras
This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0.
This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version, with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |