Liking cljdoc? Tell your friends :D

value-type

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.

Project status

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.

Installation and quick start

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"]}}}

Development only

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"]}}}

JDK requirement

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.

Example

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.

Non-nil fields

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.

Value arrays

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.

Important behavior and limitations

  • Value objects have no identity. Equality depends on their fields; identity-based operations such as locking are not supported.
  • Mutable fields are not supported in either representation.
  • Fallback uses ordinary deftype identity semantics. It does not emulate value equality. Code that permits fallback must work with both forms.
  • Ahead-of-time compilation fixes representation and field enforcement. Value-class output requires a compatible preview-enabled JVM; it does not convert to fallback. Its guard rejects evaluation when value-class capability is unavailable, with :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:

ValueBehavior when fallback is needed
silent (default)Use deftype without a diagnostic.
warnWarn once per loaded library runtime, then use deftype.
errorReject 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.

Performance

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.

Documentation

AI usage

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.

License

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

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close