Skip to content

multiplyco/scoped

Repository files navigation

Scoped

Clojars Project

A Clojure/ClojureScript library for scoped values. On the JVM, uses Java 25's ScopedValue API for efficient context propagation with virtual threads. In ClojureScript, falls back to binding.

Requirements

Clojure:

  • JDK 25+ (recommended, LTS)
  • Clojure 1.12+

ScopedValue was a preview feature in JDK 21-24 and became stable in JDK 25. It may work on earlier versions with --enable-preview, but this is untested and the API may have changed between versions.

ClojureScript:

  • Any supported ClojureScript version

Installation

;; deps.edn
co.multiply/scoped {:mvn/version "0.1.x"}

Why scoped values?

Clojure's binding uses thread-local storage, which doesn't propagate to child threads. This is problematic with virtual threads where work is frequently dispatched across threads.

ScopedValue (introduced in Java 21, GA in Java 25) provides immutable bindings that are significantly more efficient than thread-locals, making them well-suited for both virtual threads and platform threads. Unlike thread-locals, they have minimal overhead per binding and per-thread access.

This library emerged while working on async code where it became clear that extracting and setting thread bindings accounted for the vast majority of the overhead involved. Switching to scoped values cut about 95% of the overhead: from ~20μs to ~1μs per async operation.

To make it easier to use in Clojure/ClojureScript cross-compilation contexts, a ClojureScript code-path is included which falls back on standard binding, as ScopedValue has no JavaScript equivalent.

API

scoping (CLJ + CLJS)

Establish scoped bindings, similar to binding:

(require '[co.multiply.scoped :refer [scoping ask]])

(def ^:dynamic *user-id* nil)
(def ^:dynamic *request-id* nil)

(scoping [*user-id* 123
          *request-id* "abc"]
  (ask *user-id*))

;; => 123

Scopes nest naturally; inner bindings shadow outer ones:

(scoping [*user-id* 1]
  (scoping [*user-id* 2]
    (ask *user-id*)))

;; => 2

ask (CLJ + CLJS)

Access a scoped value. Falls back to the var's root binding if not in scope:

(require '[co.multiply.scoped :refer [ask scoping]])

(def ^:dynamic *user-id* :default)

(ask *user-id*)  ; => :default

(scoping [*user-id* 123]
  (ask *user-id*))

;; => 123

Throws IllegalStateException if the var is unbound and not in scope.

Gotcha: It can be easy to forget ask and reference the var directly. With a default value, this fails silently:

(def ^:dynamic *user-id* :default)

(scoping [*user-id* 123]
  (str "User: " *user-id*))  ; Oops, forgot `ask`

;; => "User: :default"  (wrong!)

Prefer unbound vars. They're more likely to fail when used, making the mistake obvious:

(def ^:dynamic *user-id*)

(scoping [*user-id* 123]
  (+ *user-id* 1))  ; Forgot `ask`

;; => ClassCastException: Var$Unbound cannot be cast to Number

current-scope (CLJ only)

Capture the current scope map for later restoration:

(require '[co.multiply.scoped :refer [current-scope scoping]])

(def ^:dynamic *user-id*)

(scoping [*user-id* 123]
  (current-scope))

;; => {#'*user-id* 123}

with-scope (CLJ only)

Restore a previously captured scope:

(require '[co.multiply.scoped :refer [ask current-scope scoping with-scope]])

(def ^:dynamic *user-id*)

(def captured
  (scoping [*user-id* 123]
    (current-scope)))

(with-scope captured
  (ask *user-id*))

;; => 123

Virtual thread example (CLJ only)

Capture and restore scope across virtual thread boundaries:

(require '[co.multiply.scoped :refer [ask current-scope scoping with-scope]])

(defmacro vt
  [& body]
  `(let [scope# (current-scope)]
     (Thread/startVirtualThread
       (fn run# [] (with-scope scope# ~@body)))))

(def ^:dynamic *user-id*)

(scoping [*user-id* 123]
  (vt (println "User:" (ask *user-id*))))

;; Prints: "User: 123"

(scoping [*user-id* 123]
  ;; Nested virtual threads
  (vt (vt (println "User:" (ask *user-id*)))))

;; Prints: "User: 123"

License

MIT License. Copyright (c) 2025 Multiply. See LICENSE.

Authored by @eneroth

About

A Clojure/ClojureScript library for scoped values

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published