Welcome! Please see the About page for a little more info on how this works.

+3 votes
in Spec by

I'm relatively new to Clojure/Spec and having read the documentation it's not immediatly obvious to me whether it is best practice is to (re)declare specs within the module they validate to or whether it is better to declare common specs seperatly for reuse across multiple modules.

For example, a game might have many different objects that all need a position consisting of x, y coordinates. In this repo for example the ::x and ::y spec defs are redeclared in each "entity" module (clouds, explosions etc). Is this preferred to having a single spec for position and importing accross modules?

2 Answers

+7 votes
by
selected by
 
Best answer

I had a discussion about this with a few others on the slack. One idea that emerged (there are many, and I might even have a second answer describing another approach) is the following:

  • Put value specs together in a shared namespace, i.e, specs.clj
  • Put structural specs in the namespaces that depend on that structure, i.e., inside the namespace itself like inside ships.clj and stars.clj or in their corresponding spec namespace like ships_specs.clj and stars_specs.clj
  • Put type specs together in a shared namespace, maybe even a shared library since you might want to use this one across even unrelated projects, such as type_specs.clj or common_specs.clj

The tricky part of this approach is deciding exactly what spec fit which category. I'm still struggling a little with this, but the following explanations help a lot.

What's a value spec?

A value spec is a spec for data that is non-divisible.

Anything that isn't a collection would be a value spec, such as a string, a number, a keyword, etc.

Sometimes, it could be a collection as well, but where in the context of your app, you never break apart that collection. This is where it gets a little fuzzy, and I recommend maybe starting without any collection as value specs, but it would be for example a point : {:x 0-255 :y 0-255}. If you'll never represent a point any differently, such as never having it be: [:x 0-255 :y 0-255] or never have a function that only takes :x coordinates without their corresponding :y for example, then it qualifies as a value spec, otherwise it doesn't.

A value spec is also meaningful to your app. It means that the name of the value spec has meaning in your application. For example, you could have (s/def ::first-name string?) and (s/def ::last-name string?). When you look at those, you see they are both just of spec string?, but in your app, it is meaningful to distinguish those string? that are first names to those that are last names. Thus all of your value specs should have meaningful names like that, which are part of the ubiquitous language that you use when discussing the domain of your app. If you explain to me what your app does, you wouldn't say it takes strings and stores them in a DB, you'd say it takes customer first names and last names.

Thus, because they are domain concepts, with a domain wide meaning, they go in your shared spec namespace.

One clarification here, if you realize you have more than one type of first-name, for example, you realize you keep talking about customer-first-name and animal-first-name. It means you need to have two value spec, one for each. Basically, if there are sub-contexts within the context of your app, where the same name is used with a different implicit meaning, you need to make that explicit.

What's a structural spec?

A structural spec is a temporary arrangement of value specs into some collection or structure. What Rich Hickey calls: "information traveling together". Sometimes, you have a function that needs three inputs, and it decides to take those on a map together, that is a structural spec, it is specific to that function and how it wants the data to be arranged during its execution, so it goes alongside its namespace.

Many more things are structural specs then you'd think at first. For example, you might think a User is a value spec. But dig a bit deeper, do you always have exactly the same information put together in exactly the same structure absolutely everywhere your app makes use of a user ? Very quickly you realize, a user sometimes exists without a date-of-birth, or without an associated shopping-list, etc. Sometimes, a user has a password hash on it, and other times it doesn't.

That's why I recommend you first try this with the rule that all collections be structural specs defined in their respective namespace (or accompanying spec namespace) which makes use of it. And only have value specs be non-collection. That really trains you into this model of speccing things, and once you get a good grasp, the few collections that are good candidates for value specs will be more easily for you to recognize.

So, things like s/keys, s/map-of, s/tuple, s/coll-of, etc. are ll most likely going to be structural specs.

In general, you don't need to spec all structures of your app. Start with only the inputs and outputs at the boundary. So user provided data, data you save to a file or a DB, data sent and returned to/by your APIs, etc. This is where it is most critical that the data be validated. Feel free to expand that to more and more internal things if you want better safety that your code works, or to document things.

What's a type spec?

Lastly, a type spec is specs whose name isn't meaningful to your app. Remember how I said value specs must have meaningful names, well, when a value spec doesn't have a meaningful name, it is a type spec. For example, we had ::first-name and ::last-name value specs, and they were both string?. Well say you want something that is only a non blank string? and you want ::first-name and ::last-name to both be of that type? Well you can create a type spec: (s/def ::non-blank-string (complement str/blank?)).

Sometimes in the type_specs namespace, instead of putting specs, you could put spec creating functions/macros as well, utils that help you define type specs. Such as say a (string-of-length len) which would return a spec validating that a string is of a given length.

That's the point of type specs and its namespace. Since those are basically domain agnostic, you could see yourself putting them in a library, which you reuse across projects.

How does it all look like together?

Well, I won't go over the entire spacewars code base, but for example, using this style of spec modeling, you might have:

  • value_specs.clj
  • (s/def ::x number?)
  • (s/def ::y number?)
  • (s/def ::velocity-1d number?)
  • (s/def ::velocity-2d ::type/num2tuple)
  • (s/def ::age number?)
  • (s/def ::direction number?)
  • (s/def ::romulan-state #{:invisible :appearing :visible :firing :fading :disappeared})
  • (s/def ::battle-state #{:no-battle :flank-right :flank-left :retreating :advancing})
  • (s/def ::battle-state-age number?)

Basically you'd put all value specs in value_specs, if one name means something different in a different context like for state and velocity you make the context explicit in its name.

  • klingons.clj
  • (s/def ::klingon (s/keys :req-un [::value/x ::value/y ::value/velocity2d ::value/battle-state-age ::value/battle-state ...] ...)

And so forth

  • type_specs.clj
  • (s/def ::num2tuple (s/tuple number? number?))

Pros/Cons

Pros

  • Forces you to understand the different names in your domain and their contexts
  • Reuse of value specs across namespaces that make use of them, instead of defining them over and over again
  • Possibly avoids having two names for the same exact thing
  • Prevents you from accidentally coupling yourself to a particular instance of a structure
  • Allows functions to freely define the structure they prefer to operate over
  • Reuse of common type specs and spec constructors/utils across projects

Cons

  • You could accidentally use the same value spec for two things that actually were not the same, and one day you realize you need to distinguish them, and thus need to factor out the value spec and break it in two.
  • You might end up with duplicate structure specs, but this is intentional, as we assume those are more likely to turn out one namespace needs more or less keys or needs it in a different collection.
by
This is a great answer and full of very useful information. I'd also be interested to see the other approach you mention in your opening sentances.
by
Really good information/guidelines! Just a note on your second "con": Spec 2 will hopefully address that by separating the set of possible keys (schema) from the specification of requiredness (select), so I would expect schema specs to be shared but select specs based on those schemas to be specific to the namespace of use (or even the function of use).
+2 votes
by

Note that ::x and ::y are different specs in each namespace because :: resolves to the current namespace, so those are really :a.b/x and :c.d/x (if they were in those namespaces).

If they are meant to represent the same entities across multiple namespaces, they should be placed in a specific namespace that is required from those other namespaces.

As for placement, it depends. A lot of people will put data-related specs in their own namespaces, grouped by domain concerns, but put function-related specs in the same namespace as the functions they "belong" to. That's mostly what we do at work.

I blogged about our use of Spec at work which is by no means definitive but seems to be fairly common practice.

by
The blog post helped a lot with my understanding of what spec is actually being used for in experienced teams, so thank you for the link.
...