One Fish Spec Fish
Clojure.spec is an exciting, new core library for Clojure. It enables pragmatic specifications for functions and brings a new level of robustness to building software in Clojure, along with unexpected side benefits. One of which is the ability to write specifications that generate Dr. Seuss inspired rhymes.
In this blog post, we’ll take a tour of writing specifications for a clojure function, as well as the power of data generation. First, some inspirational words:
1 2 3 4 |
|
The mere shape of these words brings a function to mind. One that would take in a vector:
1
|
|
and give us back a string of transformed items with the word fish added, of course.
But, let us turn our attention the parameters of this function and see how we can further specify them. Before we get started, make sure you use the latest version of clojure, currently [org.clojure/clojure "1.9.0-alpha13"]
, test.check [org.clojure/test.check "0.9.0"]
, and add clojure.spec to your namespace.
1 2 |
|
Specifying the values of the parameters
Back to the parameters. The first two are integers, that’s pretty easy, but we want to say more about them. For example, we don’t want them to be very big. Having a child’s poem with the One Hundred Thousand and Thirty Three fish really won’t do. In fact, what we really want is to say is there is finite notion of fish-numbers and it’s a map of integer to string representation.
1 2 3 |
|
Then, we can use the s/def
to register the spec we are going to define for global reuse. We’ll use a namespaced keyword ::fish-number
to express that our specification for a valid number is the keys of the fish-numbers
map.
1
|
|
Now that we have the specification, we can ask it if it’s valid for a given value.
1 2 |
|
So 5
is not a valid number for us. We can ask it to explain why not.
1 2 |
|
Which, of course, totally makes sense because 5
is not in our fish-numbers
map. Now that we’ve covered the numbers, let’s look at the colors. We’ll use a finite set of colors for our specification. In addition to the classic red and blue, we’ll also add the color dun.
1
|
|
You may be asking yourself, “Is dun really a color?”. The author can assure you that it is in fact a real color, like a dun colored horse. Furthermore, the word has the very important characteristic of rhyming with number one, which the author spent way too much time trying to think of.
Specifying the sequences of the values
We’re at the point where we can start specifying things about the sequence of values in the parameter vector. We’ll have two numbers followed by two colors. Using the s/cat
, which is a concatentation of predicates/patterns, we can specify it as the ::first-line
1
|
|
What the spec is doing here is associating each part with a tag, to identify what was matched or not, and its predicate/pattern. So, if we try to explain a failing spec, it will tell us where it went wrong.
1 2 3 |
|
That’s great, but there’s more we can express about the sequence of values. For example, the second number should be one bigger than the first number. The input to the function is going to be the map of the destructured tag keys from the ::first-line
1 2 |
|
Also, the colors should not be the same value. We can add these additional specifications with s/and
.
1 2 3 |
|
We can test if our data is valid.
1
|
|
If we want to get the destructured, conformed values, we can use s/conform
. It will return the tags along with the values.
1 2 |
|
Failing values for the specification can be easily identified.
1 2 3 4 |
|
With our specifications for both the values and the sequences of values in hand, we can now use the power of data generation to actually create data.
Generating test data - and poetry with specification
The s/exercise
function will generate data for your specifications. It does 10 items by default, but we can tell it to do only 5. Let’s see what it comes up with.
1 2 3 4 5 6 |
|
Hmmm… something’s not quite right. Looking at the first result [0 1 "Dun" Red"]
, it would result in:
1 2 3 4 |
|
Although, it meets our criteria, it’s missing one essential ingredient - rhyming!
Let’s fix this by adding an extra predicate number-rhymes-with-color?
.
1 2 3 4 |
|
We’ll add this to our definition of ::first-line
, stating that the second number parameter should rhyme with the second color parameter.
1 2 3 4 5 6 7 8 9 |
|
Now, let’s try the data generation again.
1 2 3 4 5 6 7 8 9 10 11 |
|
Much better. To finish things off, let’s finally create a function to create a string for our mini-poem from our data. While we’re at it, we can use our spec with s/fdef
, to validate that the parameters are indeed in the form of ::first-line
.
Using spec with functions
Here’s our function fish-line
that takes in our values as a parameters.
1 2 3 4 5 6 7 |
|
We can specify that the args for this function be validated with ::first-line
and the return value is a string.
1 2 3 |
|
Now, we turn on the instrumentation of the validation for functions and see what happens. To enable this, we need to add the spec.test
namespace to our requires:
1 2 3 |
|
The instrument function takes a fully-qualified symbol so add ` before the function name to resolve it in the context of the current namespace.
1 2 3 4 |
|
But what about with bad data?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Ah, yes - the first number must be one smaller than the second number.
Wrap up
I hope you’ve enjoyed this brief tour of clojure.spec. If you’re interested in learning more, you should check out the spec.guide. It really is an exciting, new feature to Clojure.
In the meantime, I’ll leave you with one of our generated lines, sure to be a big hit with future generations.
1 2 3 4 |
|