I’ve been using Clojure Spec at work for about 4 months now. I’ve never really worked with anything quite like Spec, and I still feel like I’m only scratching the surface with how I’m using it.

Here is some of the thought processes/trains that have been passing before my programming-mind’s eye regarding Spec:

Thought Train

Maps in Maps in Maps

I’m starting to wrap my head around how to spec large data sets, like a user profile, in which there are several smaller sets of data within the parent (ex. address, posts, images, whatever).

On first approach, I imagined spec’ing a composite form to be like so:

;; a map with keys and their associated predicates.
:profile {:name: string?
          :email: email-regex?
          :business {:name string?}
          :address {:city #{set of possible cities}
                    :street string?
                    :postal-code string?}
          :... "and so on"
          }

All of the above, would have a spec ran against the entire map, then converted to JSON before being shoved into the database or sent over the wire to my front end.

This is not how spec works, as far as I can tell. There are tools that seem to be useful for achieving something like above, however[1]. Instead, Spec wants you to create singular spec defintions and their associated predicates for each and every “field”, which can then later be composed into larger compound specs. When it comes to combining spec definitions to make something larger (such as a :profile definition) it can be a bit challenging. In the example above, we might start defining some specs like so:

(ns lib.user
  "Specifications for a user."
  (:require [cljs.spec :as spec]
            [lib.common :as common]))

(spec/def ::name     common/str-1->256?)
(spec/def ::email    common/email?)

Verrrrrry reasonable!

But what about when we want to add specs for the user’s business?

 1 2 3 4 5 6 7 8 9101112131415161718192021222324252627
(ns lib.user
  "Specifications for a user."
  (:require [cljs.spec :as spec]
            [lib.common :as common])) ;; < just some general predicates I use.

(spec/def ::name     common/str-1-256?)
(spec/def ::email    common/email?)

;; ah here's a problem, how do we spec the :name key under :business?
;; change the key name?:
(spec/def ::business-name  common/str-1->256?)

;; that seems a little barbaric no? 
;; At that point why don't we namespace EVERY key and make it one flat map?
;; AND WHY DONT WE BURN OUR CLOTHES AND MAKE NEW ONES FROM THE CLOUDS?
;; No. I want to organize the domain so that when my cursor descends into a map, 
;; my brain does too, and I've shifted contexts.

;; name space it differently:
(spec/def :lib.user.business/name common/str-1->256?)
(spec/def :lib.user.business/email common/email?)

(spec/def ::new-user
  (spec/keys :req-un [::name 
                      :email 
                      :lib.user.business/name
                      :lib.user.business/email]))

Now imagine this but for a data structure with several maps within it, with possible different keys. It grows a bit unwieldy fairly quickly, but it’s also the most clean vanilla-spec way I’ve found yet.

This idea of building semi-descriptive namespace keywords that sort of chart the context of a leaf gets brought up in this helpful post. The global keyword registry (: vs ::) is a bit confusing at first, but it has slowly become clear thanks to continued practice with using re-frame’s events/subscriptions.

Still… this feels a bit verbose. I find myself writing blocks of code comments describing that I’m “in the process of building a composite spec” – working my way up from each leaf of a sub map, until I get to the parent compound spec. I can imagine this being particularly frustrating for future programmers to read;

Until now, I’ve been occaisionally splitting some specs into seperate namespaces by files (especially for address/location related things), but I think going forward the use of “nested” namespaces seperated by . (dots) provides enough of a “pathway” into describing the data.

On the front end

I won’t get into it much in this topic, but the specs I’m defining are being shared between multiple cljs front ends and a cljs back end. The usefulness of singular spec defintion fields (as opposed to spec’ing a giant map in my first example) revealed itself to me after starting to use Spec for front end validations (and subsequent ui feedback). Every field, whether it’s for a user’s name, or their business’ name, needs it’s own spec, that gets run EVERY time a user types. Just typing that out makes it seem like a bit overkill, but I’ve found that once I’m happy with the organization of the specs, at least, it’s not so troublesome to see them being imported from their respective namespaces and used across various front ends. I’ll save that for another log entry.

Ideally, I’d have a single datastructure that I could define forms from and validation all declaritively. Every time I try and do something like that, however, it’s like walking around with a pair of pants that are on fire – but sort of slowly smouldering upwards, until before long the flames grow and grow and I have to quickly change pants. In this metaphor, pants are code. But you already knew that.

Anyway, singular specs need to exist; and rather, would not exist if they were balled up in a single schema definition of a single data structure (Spec tools seems to allow this, but the definitions are generated and so, from my cursory glance, doesn’t see useable on a form-field by form-field basis).

I think a good chunk of my hesitation with Spec (other than it’s changing API and error messages) is that I’m writing seemingly more code than I have elsewhere in Clojure, and that I have perhaps gotten used to expecting that there would be a more pithy way to go about things.

My adventures with Spec will go on!

Other links

https://spin.atomicobject.com/2017/06/27/clojure-spec-json-data/

Footnotes

[1] Spec tools looks cool, but I won’t use it for several reasons: I don’t want to use a library to make something easier, when I can’t even say what’s hard about it yet – I really need to actually try and use a tool and know the pain points before claiming I need another library to help. On top of that, once I know what my pain points are (which I guess I’m trying to textually process in this log) I’ll try and hand roll something for the domain first before reaching for something more general purpose.