Clojure's deftype creates objects with identity. value-type provides
defvaluetype to define JEP 401 value objects
with familiar deftype syntax. It uses value classes on a supported
Valhalla JVM, unless value-type.value-classes is off, and ordinary
identity classes otherwise, subject to application fallback policy.
Required, typed fields retain their constraints in either representation.
Libraries that emit deftype*, such as closed-record, can use
defvaluetype* for plain, defrecord-shaped and closed record classes. See
deftype* forms.
For collections of value objects, value-type.array creates native arrays
that reject null elements and provide atomic element access. The JVM can
store values directly in the array instead of through separate object
references. This storage layout is called flattening; it is not guaranteed.
The project is experimental. Do not assume a stable API or production readiness. Value-class support depends on an early-access JVM.
Release 0.1.127 coordinates are prepared. Use them only after this version is published by the release workflow.
Tests cover Clojure 1.12.6 on the primary Java 8 and Valhalla builds in the JVM facts, with preview disabled and enabled on Valhalla. This is tested coverage, not a guarantee for future JVM versions. See the verification commands.
After version 0.1.127 is published, add these Clojars coordinates to
your application's deps.edn:
{:deps {org.hugoduncan/value-type {:mvn/version "0.1.127"}}
:aliases {:dev {:jvm-opts ["--enable-preview"]}}}
Cloning and :local/root installation are for development only.
Install the Clojure CLI and clone this repository:
git clone https://github.com/hugoduncan/value-type.git
In your application, add this to deps.edn. Replace <value-type-dir>
with the path to the cloned repository:
{:deps {org.hugoduncan/value-type {:local/root "<value-type-dir>"}}
:aliases {:dev {:jvm-opts ["--enable-preview"]}}}
For value classes, select a supported Valhalla JDK as described in the
JDK setup guide. Set JAVA_CMD
to its bin/java, then start a REPL from your application directory:
export JAVA_CMD=<valhalla-jdk-home>/bin/java
clojure -M:dev
To check which value-class and array features a deployment supports, see Capability report.
Define and construct a value type:
(require '[value-type.core :refer [defvaluetype]])
(defvaluetype Point [^long x ^long y]
Object
(toString [_] (str "(" x "," y ")")))
(str (->Point 1 2)) ;=> "(1,2)"
(identical? (->Point 1 2) (->Point 1 2)) ;=> true
(.isValue Point) ;=> true
The last two results require supported value classes with preview enabled.
To try ordinary-JVM fallback, start with clojure -M instead. Construction
still works, but the two instances are not identical, and Class.isValue
can be absent.
Use an explicit concrete-class tag and :non-nil metadata to require a
non-null nested value:
(defvaluetype Segment [^Point ^:non-nil start
^Point ^:non-nil end])
(def segment (->Segment (->Point 1 2) (->Point 3 4)))
(.-x (.-start segment)) ;=> 1
These declarations also work on ordinary JVMs when fallback policy permits identity classes. Constructors retain concrete field types and reject nil and wrong types. Eligible value fields use native enforcement when available; other fields use generated constructor checks. Neither path guarantees flattening. Field enforcement needs no internal-package export.
Tags can name already defined concrete identity or value classes, including
String. The old :nil-restricted key is rejected, even when false or nil.
See the field contract for eligible tags,
errors and deployment limits.
Array construction needs a supported Valhalla JVM, preview, access to an
internal JVM package and value-type.value-classes not off. With
JAVA_CMD set as above, start a new REPL:
clojure -J--add-exports=java.base/jdk.internal.value=ALL-UNNAMED -M:dev
Evaluate the Point definition above, then create and use arrays:
(require '[value-type.array :as va])
;; Allocate three slots, each with the supplied initial value.
(def points (va/make-array Point 3 (->Point 0 0)))
(alength points) ;=> 3
(aset points 1 (->Point 4 5))
(str (aget points 1)) ;=> "(4,5)"
;; Build an array from finite input, in input order.
(def path (va/into-array Point [(->Point 1 2) (->Point 3 4)]))
(mapv str path) ;=> ["(1,2)" "(3,4)"]
;; Empty input needs an explicit non-null seed; the seed adds no slot.
(alength (va/into-array Point (->Point 0 0) [])) ;=> 0
;; Inspect actual JVM properties, including whether storage is flat.
(select-keys (va/array-info points) [:null-restricted? :atomic?])
;=> {:null-restricted? true, :atomic? true}
(:flat? (va/array-info points)) ;=> true or false, depending on layout
These are mutable arrays of immutable value objects. Use ordinary array
operations such as aget, aset and alength. Stores of null or incompatible
values fail. Atomic element access does not make compound updates atomic or
remove the need for thread coordination.
Unlike non-nil fields, array construction has no ordinary-JVM fallback.
into-array buffers the input before allocation; it is not a streaming
constructor. array-info also accepts ordinary reference arrays. It needs
the export flag, but ordinary-array inspection does not need preview on the
facts-selected Valhalla build.
Public require alone does not load the optional array adapters. First successful
allocation or inspection discovery loads and compiles the relevant source
adapter, then caches its direct-call functions. A capability report can
trigger this discovery and its first-use cost.
See the API reference for all three functions and the usage guide for validation and error handling.
deftype identity semantics. It does not emulate
value equality. Code that permits fallback must work with both forms.:value-capability-unavailable, at namespace
initialization or function invocation. Identity output retains its constructor
checks. See AOT limits.The property value-type.fallback controls identity fallback, including
definitions with non-nil fields. Missing native field enforcement alone does not
trigger fallback, warnings or strict-policy rejection:
| Value | Behavior when fallback is needed |
|---|---|
silent (default) | Use deftype without a diagnostic. |
warn | Warn once per loaded library runtime, then use deftype. |
error | Reject with java.lang.Error. |
The policy applies if it is set before the first read: the first fallback
decision or the first value-type.capability/report call. A namespace with a
top-level fallback definition makes the first read when it loads, so the JVM
option is the recommended form. For example, to reject fallback:
clojure -J-Dvalue-type.fallback=error -M:dev
A System/setProperty call, for example in -main, applies only if no policy
read occurred before it. report shows the policy in effect in
:fallback-policy.
Set value-type.value-classes=off to select identity fallback for new
definitions, subject to fallback policy, for example after a JVM value-class
defect. Saved value output cannot convert to identity classes: its guard
rejects evaluation with :value-capability-unavailable and reason :disabled.
See the usage guide for field comparison rules, fallback policy, value-class switch, AOT limits and error handling.
Value classes do not guarantee lower memory use or faster execution. In the
recorded Valhalla tests, standalone defvaluetype objects with 1–3 long
fields were 8 bytes larger than equivalent deftype objects. Empty objects
had equal shallow sizes. Method-call measurements found no stable
interface-call penalty. These results apply only to the tested JVM and
workloads; they do not measure flattened arrays or total application memory.
See the method-call investigation and object-size comparison for methods, results and limits. Measure your own workload before choosing a representation for performance.
Separate typed-array measurements
found storage and traversal benefits for flattened arrays of single-long
values on the tested JVM. These are workload-specific results, not a promise
for other value types or layouts. The array benchmark
uses the public allocator; the retained measurements used the internal JVM
allocator directly and do not measure into-array construction.
The direct-call adapter comparison measured lower warmed operation times but about 3–4 ms additional first-use time for lazy source loading and compilation. These results apply only to the recorded host, JVM build and workload; they do not guarantee a speed increase.
AI has been used to write and review code, tests and documentation for this project. Automated tests and AI review do not guarantee correctness. Evaluate the library for your intended use.
Eclipse Public License 2.0 (EPL-2.0). The full license text is included in the source checkout and the built JAR.
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 |