Chapter 5
Specifications

Statically typed languages like Java create unique, named data structures (classes) for every unit of data in a program. Clojure is dynamically typed and instead relies on reusing a few generic collection types (vectors, maps, sets, lists, and sequences) to represent a program’s data. This approach yields tremendous opportunities for code reuse, simplicity, generality, and extension.

However, one consequence of this approach is that functions in a Clojure program lack the explicit types that programmers in a statically typed language rely on as signposts to understand a piece of code.

For example, a Java program might have a function that takes a recipe ingredient and scales the quantity for a larger recipe. In Java, we would define an Ingredient class and Unit class, and the method might look like this:

 public​ ​class​ Ingredient {
 private​ String name;
 private​ ​double​ quantity;
 private​ Unit unit;
 
 // ...
 
 public​ ​static​ Ingredient scale(Ingredient ingredient, ​double​ factor) {
  ingredient.setQuantity(ingredient.getQuantity() * factor);
 return​ ingredient;
  }
 }

In a Clojure program, an ingredient might just be a map with some well-known keys. A scale function would then take and return a map without ever mentioning any explicit types:

 (​defn​ scale-ingredient [ingredient factor]
  (update ingredient :quantity * factor))

This code is preferable to the Java version in several ways. It takes and returns immutable values, rather than update mutable state, so it’s easier to reason about and inherently thread-safe by default. Also, the ingredient maps are just collections of attributes that can easily evolve over the life of a program, rather than being trapped behind the custom API of a Java class. However, one thing the Clojure code is lacking is explicit information about the expected structure of ingredient. We need to infer the structure from documentation or other parts of the program.

In Clojure 1.9, Clojure introduced the spec library, which allows us to create “specs” that describe the structure of our data, and the inputs and outputs of our functions. The actual code we write is the same—it still uses generic collections and generic operations on those collections. Adding specs to your code can make the implicit structure explicit.

For example, we could annotate this code with the following specs (s here is an alias for the clojure.spec.alpha namespace):

 ;; Specs describing an ingredient
 (s/def ::ingredient (s/keys :req [::name ::quantity ::unit]))
 (s/def ::name string?)
 (s/def ::quantity number?)
 (s/def ::unit keyword?)
 
 ;; Function spec for scale-ingredient
 (s/fdef scale-ingredient
  :args (s/cat :ingredient ::ingredient :factor number?)
  :ret ::ingredient)

These specs give us a precise description of the shape of an ingredient map, its fields, and their contents. The function spec gives us an explicit definition of the arguments and return value of scale-ingredient. These specs don’t just serve as documentation. The spec library uses them to provide several additional tools that operate on specs—data validation, explanations of invalid data, generation of example data, and even automatically created generative tests for functions with a spec.

We’ll start by considering how to define specs in our code and use them at runtime, followed by how we combine specs and validate data. Finally, we’ll see how to use s/fdef to define function specs for argument checking and generative testing.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.172.56