DJ" rel="icon" type="image/svg+xml">Things I wish Datomic had: Map valuescode • words • emotions<br>Daniel Janus’s blog
blog po polsku<br>RSS<br>home page
Things I wish Datomic had: Map values<br>24 July 2026 • programming<br>Clojure<br>Datomic<br>Datahike
Whence this post<br>JEP 401 – a proposal to add value classes to Java – has been crystallizing for almost six years now, but I only learned about it this morning. I skimmed it, nodding along to myself as I thought “gee, I can’t imagine going back to Java” and “wonder how this might make Clojure more performant?”. But then I thought about some Datomic (actually, Datahike) data remodelling that I’d been working on recently, and I realized that the two areas are connected.<br>So here’s a braindump, in an effort to clear up my mental image of all this.<br>A simple example<br>Consider this Clojure value:<br>(def prog<br>{:program/name "Apache Maven"<br>:program/url "https://maven.apache.org"<br>:program/version {:version/major 3<br>:version/minor 9<br>:version/patch 16}})<br>If you’re like me, you’ll have a warm, comfortable feeling in your heart looking at this. Plain data at rest. Accessible, transformable. What’s not to love?<br>Well, now try storing it in Datomic. Easy! Let’s first define a schema:<br>(def schema<br>[{:db/ident :program/name<br>:db/valueType :db.type/string<br>:db/unique :db.unique/identity<br>:db/cardinality :db.cardinality/one}<br>{:db/ident :program/url<br>:db/valueType :db.type/uri<br>:db/cardinality :db.cardinality/one}<br>{:db/ident :version/major<br>:db/valueType :db.type/long<br>:db/cardinality :db.cardinality/one}<br>{:db/ident :version/minor<br>:db/valueType :db.type/long<br>:db/cardinality :db.cardinality/one}<br>{:db/ident :version/patch<br>:db/valueType :db.type/long<br>:db/cardinality :db.cardinality/one}<br>{:db/ident :program/version<br>:db/valueType :db.type/ref<br>:db/isComponent true<br>:db/cardinality :db.cardinality/one}])<br>And now we can transact it (I’ll assume we have a DB connection, conn):<br>@(d/transact conn schema)<br>@(d/transact conn [prog])<br>Done. Now we can check what Maven’s version is:<br>(let [db (d/db conn)<br>mvn (d/entity db [:program/name "Apache Maven"])]<br>(:program/version (d/touch mvn)))<br>;=> {:db/id 17592186045419, :version/major 3, :version/minor 9, :version/patch 16}<br>It works!<br>Meh<br>But I’m not exactly happy about this.<br>What I’d really like to get is #:version{:major 3, :minor 9, :patch 16}. But Datomic models maps as entities – focal points that bind attributes with values. That’s fine for the top-level program map, but version is not an entity! It’s just a value, like a number or a string. It has internal structure, but, conceptually, it’s just an atomic value, an element of the set of all possible major.minor.patch version numbers.<br>Yet Datomic forces us to “reify” the version map as an entity. That means that it automatically gets a :db/id. If a new version of Maven gets released, and we transact that fact:<br>@(d/transact conn<br>[[[:program/name "Apache Maven"]<br>:program/version<br>#:version{:major 3, :minor 9, :patch 17}]])<br>then Datomic will create a fresh artificial entity, give it the three version attributes, and associate it with the Maven entity. But the old one still exists in the DB! It’s “semi-orphaned” (detached from the rest of the object graph in the current state, but still reachable via history). If Maven for some reason goes back to 3.9.16 via a similar transaction, then we’ll get a third entity that is a duplicate of the first one.<br>Even worse, there’s nothing stopping us from changing attributes of that entity:<br>(let [db (d/db conn)<br>mvn (d/entity db [:program/name "Apache Maven"])]<br>@(d/transact conn [[(-> mvn :program/version :db/id) :version/major 4]]))<br>Now the identity of version hasn’t changed, but Maven is at 4.9.17, and every other entity that happened to be referencing the artificial version one is at 4.9.17 too! Clearly, this kind of thing should be disallowed.<br>Also note that I had to say :db/isComponent true in the schema for the transaction to have succeeded at all. isComponent means that the child entity only makes sense in the context of parent; otherwise, I’d have to lift the version map to the top-level of the transaction, give it a temporary id, and use that id to refer to it in the program map.<br>Two kinds of maps<br>At this point, I realize there are two kinds of maps: those that describe snapshots of state of some stateful entity at some point in time (like program), and those that are merely juxtapositions of named values (like version). For want of better names, for now on I’ll call these “snapshot maps” and “plain maps”, respectively.<br>Quoting from Clojure’s Approach to Identity and State:<br>We need to move away from a notion of state as "the content of this memory block" to one of "the value currently associated with this identity". Thus an identity can be in different states at different times, but the state itself doesn’t change. That is, an identity is not a state, an identity has a state. Exactly one state at any point in time. And that state is a true value, i.e. it never changes.
With this in...