on the clojure move

jump to main content

De-complecting clojure.spec

Last week I presented "De-complecting clojure.spec" at Clojure South 2025, exploring how to evolve clojure.spec from a validation tool into a foundation for self-discoverable semantic services.

The key insight: by changing spec's registry from a 1-to-1 mapping (namespaced-keyword → spec) to a 1-to-N mapping (namespaced-keyword → {namespaced-keyword → value|spec}), we can create RDF-compliant semantic systems while solving Rich Hickey's "Maybe Not" problem of context-dependent requirements.

This article walks through that design evolution.

1. From validation to Self-Discoverable Semantic Services

So, the "De-complecting clojure.spec" talk contained following 14 headlines:

[:s-dss.woody-allen.annie-hall.script/show-me-now
 :s-dss.clojure.spec.hidden/code
 :s-dss.talk/me&yorba.co
 :s-dss.clojure.spec.misconceptions/semantic-communication-how-to
 :s-dss.clojure.spec.prior-art/rdf     
 :s-dss.namespaced-keywords.friends/spec
 :s-dss.clojure.spec.misconceptions/spec-vs-core
 :s-dss.clojure.semantic/identities
 :s-dss.talk.semantic.service.example/docs
 :s-dss.clojure.spec.misconceptions/rdf
 :s-dss.talk.semantic.service.example/system-components
 :s-dss.clojure.spec.misconceptions/not-a-type-system
 :s-dss.clojure.spec.misconceptions/spec-registry-dimensions
 :s-dss.namespaced-keywords.semantic.analogies/file-and-dirs]

Strange that headlines were namespaced keywords, don't they? The intention of using namespaced keywords as headlines, besides enriching semantically the data was to explore these headlines using … clojure :!

Sorted headlines …

[:s-dss.clojure.semantic/identities
 :s-dss.clojure.spec.hidden/code
 :s-dss.clojure.spec.misconceptions/not-a-type-system
 :s-dss.clojure.spec.misconceptions/rdf
 :s-dss.clojure.spec.misconceptions/semantic-communication-how-to
 :s-dss.clojure.spec.misconceptions/spec-registry-dimensions
 :s-dss.clojure.spec.misconceptions/spec-vs-core
 :s-dss.clojure.spec.prior-art/rdf
 :s-dss.namespaced-keywords.friends/spec
 :s-dss.namespaced-keywords.semantic.analogies/file-and-dirs
 :s-dss.talk/me&yorba.co
 :s-dss.talk.semantic.service.example/docs
 :s-dss.talk.semantic.service.example/system-components
 :s-dss.woody-allen.annie-hall.script/show-me-now]

Only applying a clojure/sort on this vector returns a visual almost grouped view of same data. As clojure developers could quickly get other views on same data using group-by namespace-criteria ….

Criteria example: Group-by second value on namespace path

(group-by
 (fn [k] (-> (namespace     k)
             (clojure.string/split #"\.")
             (nth 1)))
 (vec (sort (:clojure-south.spec.talk/headlines @registry))))

result

{"clojure"
 [:s-dss.clojure.semantic/identities
  :s-dss.clojure.spec.hidden/code
  :s-dss.clojure.spec.misconceptions/not-a-type-system
  :s-dss.clojure.spec.misconceptions/rdf
  :s-dss.clojure.spec.misconceptions/semantic-communication-how-to
  :s-dss.clojure.spec.misconceptions/spec-registry-dimensions
  :s-dss.clojure.spec.misconceptions/spec-vs-core
  :s-dss.clojure.spec.prior-art/rdf],
 "namespaced-keywords"
 [:s-dss.namespaced-keywords.friends/spec
  :s-dss.namespaced-keywords.semantic.analogies/file-and-dirs],
 "talk"
 [:s-dss.talk/me&yorba.co
  :s-dss.talk.semantic.service.example/docs
  :s-dss.talk.semantic.service.example/system-components],
 "woody-allen" [:s-dss.woody-allen.annie-hall.script/show-me-now]}

To "decouple clojure.spec from validation to self-discoverable semantic services", is necessary to understand the clojure.spec RDF relation.

RDF is not just a "another" thing. The "Resource Description Framework" is a framework for representing information in the Web that enables machines to understand meaning

2. [subject predicate object] RDF triples

RDF (Resource Description Framework) uses triples to represent information in a way that machines can understand. Each triple has three parts:

  • Subject: The identity/entity we're describing (IRI/URI, e.g., http://example.org/person/123)
  • Predicate: The relationship or property (e.g., "hasEmail", "livesIn")
  • Object: The value (e.g., "user@example.com", "New York")

For example:

subject predicate object
http://example.org/person/123 hasEmail user@example.com
http://example.org/person/123 livesIn Sevilla

The power of RDF is that diverse predicates allow rich queries. We can ask: "Who lives in New York?" or "What email addresses do we have?" because predicates convey semantic meaning.

3. clojure.spec/registry: Limited RDF Semantics

let's try to represent a clojure.spec registry as a collection of RDF triples …

;; … in any file of my project …
(s/def :my-app.endpoint/user ….)
(s/registry)
;; => {:my-app.endpoint/user  #object[clojure.spec.alpha$spec_impl$reify__2046 0x78b3cbd "clojure.spec.alpha$spec_impl$reify__2046@78b3cbd"]}

;; … in any file of my project …
(s/def :my-app.endpoint/account ….)
(s/registry)
;; => {:my-app.endpoint/user  #object[clojure.spec.alpha$spec_impl$reify__2046 0x78b3cbd "clojure.spec.alpha$spec_impl$reify__2046@78b3cbd"]
;;     :my-app.endpoint/account  #object[clojure.spec.alpha$spec_impl$reify__2398 0x3455ghy "clojure.spec.alpha$spec_impl$reify__2398@3455ghy"]
identity predicate value
:my-app.endpoint/user spec:hasSpec clojure.spec.alpha$specimpl$reify_2046@78b3cbd .
:my-app.endpoint/account spec:hasSpec clojure.spec.alpha$specimpl$reify_2398@3455ghy" .

So, clojure.spec/registry is not semanticaly-RDF interesting thus all triples that we can generate share the same predicate. So the only interesting thing here is to query by identity only…

[:find ?value
 :where [?e :identity :my-app.endpoint/user]
        [?e :spec/hasSpec ?value]]

So, here the paradox: clojure.spec is not a type system (s/def ::user (s/keys :req [::mail ::tel])) but the RDF intention is not any "useful" either ….

4. semantic registry to (useful) RDF triples

… or, how we could def(ine) other and better RDF predicates?

let's try to do the same as spec, but with our own service and our own registry

(my-service/def ::foo ...)

(my-service/registry
;;=> {::foo ...}

BUT, and here the important distintion, instead of 1-namespaced-keyword to 1-spec-impl, let's add one more dimension to my-service/def Let's use a 2 dimension value, meaning a clojure kw-value map…

(my-service/def ::foo {::bar true ::zoo false ::opt 1})

so, now the triples that we are generating are totally different …

identity predicate value
::foo ::bar true
::foo ::zoo false
::foo ::opt 1

and so far so on …

(my-service/def ::pop {::bar false ::zoo false ::opt 2})
identity predicate value
::foo ::bar true
::foo ::zoo false
::foo ::opt 1
::pop ::bar false
::pop ::zoo false
::pop ::opt 2

Resulting on a clasical RDF system with commons expressive expectations …

[:find ?identity
 :where [?identity ::zoo false]]

;=> #{[::foo] [::pop]}

This semantic registry design enables rich querying, but it doesn't yet solve a critical problem identified by Rich Hickey in his "Maybe Not" talk: how do we handle context-dependent requirements without creating an explosion of types?

5. Solving "Maybe Not": Context-Dependent Specs

In his "Maybe Not" talk, Rich Hickey identified a fundamental problem with putting optionality in aggregate definitions: requirements are context-dependent, not inherent to the data itself. You might need `::user/email` when authenticating but not when displaying a user list.

The traditional clojure.spec approach creates a 1-to-1 mapping problem:

1-1 clojure.spec approach, maybe not endless type problem

(my-service/def ::user (s/keys :req [::tel] :opt [::email ::id])) 

;; we only can fetch one spec per one namespaced-keyword 
(::user (s/registry))
;;=> #object[clojure.spec.alpha$spec_impl$reify__2053 0x6eb3e690 "clojure.spec.alpha$spec_impl$reify__2053@6eb3e690"]

1-N semantic design approach, spec-type per context

 (my-service/def ::user
   {:my-service.context/foo    (s/keys :req [::tel ::email])
    :other-service.context/bar (s/keys :req [::tel ::id])}) 

In the example above, the namespaced-keyword ~::user~ 
;; here ::user has 2 specs related, thus we can fetch spec per (semantic) namespaced-keyword context.

 (:my-service.context/foo (my-service/fetch ::user))
 ;=> (s/keys :req [::tel ::email]) {:my-service.context/foo    

 (:other-service.context/bar (my-service/fetch ::user))
 ;;=> (s/keys :req [::tel ::id])


By moving from a 1-to-1 (namespaced-keyword → spec) to a 1-to-N (namespaced-keyword → {namespaced-keyword → value|spec}) registry design, we solve the "Maybe Not" problem while simultaneously making our system RDF-compliant. Let's see what this enables…

6. Open your system: clojure definitions are now RDF-compliant!

With this semantic approach, clojure definitions of any type (docs, system components, endpoints, metrics, workflows, warnings…) become discoverable using universal IRIs/URIs (or in Clojure dialect, namespaced keywords).

We can query our semantic registry from any part of the app code, the same as we do when (s/valid? ::foo ...). That's to say, we only need to use a literal semantic namespaced identifier (aka namespaced keyword) to use all the semantic information associated (eg: :documentation/content, :my-service.context/foo or :other-service.context/bar). We don't need to stop on specs, we can create RDF relations with any value. Basically this simple 1-N design is about open data system, the same that clojure.spec is about


https://clojure.org/about/spec

Guidelines

expressivity > proof

There is no reason to limit our specifications to what we can prove, yet that is primarily what type systems do. There is so much more we want to communicate and verify about our systems.


7. Example: semantic doc example project

8. Conclusion

By recognizing that clojure.spec's registry is actually an RDF triple store with limited semantics (all predicates are `spec:hasSpec`), we can evolve it into a rich semantic system. The key moves:

  1. Change from 1-to-1 to 1-to-N mappings: Allow one identity (keyword) to have multiple contexts
  2. Use predicates as first-class values: Enable semantic querying beyond "does this spec exist?"
  3. Solve "Maybe Not": Context-dependent requirements without type explosion

This pattern extends beyond specs—any clojure definition (docs, system components, configurations) can become RDF-compliant and semantically queryable.

As clojure.spec's guidelines state: "expressivity > proof." There's no reason to limit specifications to validation. Let's use them to create self-discoverable, semantically rich systems.