My new position requires me to get familiar with the Clojure language. In intend to document what I learn in a series of posts, to serve as my personal reference notes. As a side-effect, I hope it will also be beneficial to others who want to take the same path. There are already a multitude of great tutorials available: hence, each post will focus on a specific theme, that is specific to Clojure considering that most of my experience comes from OOP.
This is the 2nd post in the Learning Clojure focus series.Other posts include:
- Decoding Clojure code, getting your feet wet
- Learning Clojure: coping with dynamic typing (this post)
- Learning Clojure: the arrow and doto macros
- Learning Clojure: dynamic dispatch
- Learning Clojure: dependent types and contract-based programming
- Learning Clojure: comparing with Java streams
- Feedback on Learning Clojure: comparing with Java streams
- Learning Clojure: transducers
As a newcomer to Clojure, a big issue of mine is the lack of types. This is not specific to Clojure. I miss types in JavaScript, Groovy, Python, etc. While I value dynamic languages ease of use when writing scripts, my bread-and-butter is still to develop regular applications. With that in mind, I prefer to let the compiler catch type-related errors: that means focusing on actual business features, instead of writing tests to catch those errors.
While Clojure doesn’t offer types in the language syntax, its design allows to build similar capabilities via constructs. Even better, there’s an existing library to take care of that, aptly named spec.
spec is available out-of-the-box since Clojure 1.9. Earlier versions require to explicitly add the dependency on the classpath. |
Basics
To start using spec, just require the clojure.spec.alpha
package in the namespace:
(ns ch.frankel.blog.clojure.spec
(:require [clojure.spec.alpha :as sparc]))
The next step is to define the expected type of a value, using the def
function.
It accepts two parameters:
# | Name | Description |
---|---|---|
1 |
|
Symbol name |
2 |
|
Predicate |
More valid parameter values are possible, but this is quite enough to start with. |
For simple values, this is quite straightforward:
(spec/def ::nil nil?) (1)
(spec/def ::bool boolean?) (2)
(spec/def ::string string?) (3)
1 | Defines ::nil as the nil value |
2 | Defines ::bool as a boolean value |
3 | Defines ::string as any string value |
— Clojure documentation
https://clojure.org/reference/data_structures#Keywords The |
This technique is not limited to simple types. It’s also possible to restrict values to an enumeration:
(spec/def ::direction #{::NORTH ::EAST ::SOUTH ::WEST})
Spec checks
Once a spec has been def’ed, there are two different ways to use it.
- The
valid?
function returns aboolean
, depending whether a value conforms (or not) to thespec
e.g.:Source Conforms? Returns (spec/valid? ::nil nil)
true
(spec/valid? ::string "f")
true
(spec/valid? ::nil "f")
false
(spec/valid? ::string nil)
false
- The
conform?
function returns:- the value if conforms to the
spec
- or
clojure.spec.alpha/invalid
if it does’t
Source Conforms? Returns (spec/conform? ::nil nil)
nil
(spec/conform? ::string "f")
"f"
(spec/conform? ::nil "f")
clojure.spec.alpha/invalid
(spec/conform? ::string nil)
clojure.spec.alpha/invalid
- the value if conforms to the
Custom spec functions
The above code uses out-of-the-box functions e.g. nil?
and string?
.
There are a lot of similar functions.
Here’s a sample, taken from clojure.core
:
Function | Checks wether the parameter is… |
---|---|
|
a keyword (obviously…) |
|
a symbol (obviously as well) |
|
a symbol or a keyword |
|
a |
|
a |
While some use-cases are covered, a lot of specific ones are not.
In that case, any function that accepts an argument and returns a boolean
can be used.
Here’s a function that checks whether the parameter is a LocalDate
, and how it can be used:
(defn local-date?
"Check if the parameter is a java.time.LocalDate instance"
[x]
(instance? LocalDate x))
(spec/def ::date local-date?)
(spec/valid? ::date (LocalDate/of 2009 1 1)) (1)
(spec/valid? ::date "f") (2)
1 | Evaluate to true |
2 | Evaluate to false |
Spec’ing data structures
Now that we know how to spec simple values, it’s time to spec more complex ones. In Clojure, one common way to model an "entity" is to use a data map.
My favorite example is a Person
entity, with the following properties:
- First name
- Last name
- Birthdate
It can be spec’ed using the keys
function.
Parameters allow to specify which keys are required, and which ones are optional:
(spec/def ::first-name string?)
(spec/def ::last-name string?)
(spec/def ::birthdate local-date?)
(spec/def ::person (spec/keys :req [::first-name ::last-name] (1)
:opt [::birthdate])) (2)
1 | Required values |
2 | Optional value |
Here are some samples, and some associated validity checks:
Source | Returns | Rationale |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Additional entries are fine |
Spec’ing collections
The next step is to use spec to validate the type of elements in a collection, just like with generics in Java e.g List<T>
, Map<T>
or Set<T>
.
This is achieved with the help of additional functions:
coll-of
for "standard" Clojure collection,vector
orlist
, etc.map-of
for maps
For example, to spec a list of LocalDate
is quite straightforward:
(spec/def ::dates (spec/coll-of ::date))
Likewise, for a map of keyword
/ LocalDate
:
(spec/def ::map-dates (spec/map-of keyword? ::date))
Of course, it works also with data structures:
(spec/def ::map-persons (spec/map-of keyword? ::person))
Conclusion
While Clojure is a dynamically-typed language, it’s possible to supplement types by using the spec
library.
It allows to validate simple types, enumerations, maps and collections, just as with any statically-typed language.