Validating Functions

A function spec describes the operation of a function. It thus contains up to three specs: an “args” spec for the arguments of the function, a “ret” spec describing the return value, and a “fn” spec used to relate the arguments to the return value. Creating function specs unlocks many of the most useful capabilities of the spec library, like instrumentation and generative testing.

The argument spec defines the collection of arguments that you can use to invoke the function being spec’ed. The spec operations we’ve seen so far allowed us to create specs for collections, but we need more power to represent the broad range of variability we see in function arguments—repeated arguments, optional arguments, and other structured collections. For these, spec provides regex op specs.

Most languages have support for matching the characters in a string according to a regular expression,[28] which defines the structure of the string (including repeated or optional patterns). Similarly, regex op specs apply one or more specs to match the values in a collection.

Let’s see how we can use regex op specs to define the arguments in a function spec and then instrument our code to automatically validate the arguments of a function call.

Sequences With Structure

String regular expressions match the characters in a string. For example, the regular expression abc* describes strings like "ab", "abc", and "abcc". In string regular expressions, placing two matching expressions next to each other indicates concatentation. The special operator * represents the repetition of 0 or more times of the prior matching expression (in this case, c).

Regex op specs provide a similar function, but instead of matching characters in a string, they match any arbitrary value in a collection. Like string regular expressions, regex op specs can match concatenated, repeated, and optional patterns.

Perhaps the most common regex op spec is s/cat, which specifies a concatenation (a series of elements, in order), where each element is simply another spec. s/cat specs also name each component for use in conforming valid values or explaining invalid values.

For example, a spec to describe a sequential collection taking a string and an integer looks like:

 (s/def ::cat-example (s/cat :s string? :i int?))
 -> :user/cat-example
 
 (s/valid? ::cat-example [​"abc"​ 100])
 -> true

In the s/cat, each component has a keyword tag naming the component in the conformed result:

 (s/conform ::cat-example [​"abc"​ 100])
 -> {:s ​"abc"​, :i 100}

There is also a regex op spec s/alt for indicating alternatives within the sequential structure. The conformed value is an entry with the matched tag and value.

 (s/def ::alt-example (s/alt :i int? :k keyword?))
 -> :user/alt-example
 
 (s/valid? ::alt-example [100])
 -> true
 
 (s/valid? ::alt-example [:foo])
 -> true
 
 (s/conform ::alt-example [:foo])
 -> [:k :foo]

Just like string regular expressions, spec also contains operators for the repetition of a spec, which we’ll dive into next.

Repetition Operators

There are three repetition operators—s/? for 0 or 1, s/* for 0 or more, and s/+ for 1 or more.

All of the regex operators can be combined and nested arbitrarily along with predicates, sets, and other specs. The key thing to remember is that all connected regex ops describe the structure of a single sequential collection.

Consider the example of a collection that contains one or more odd numbers and an optional trailing even number:

 (s/def ::oe (s/cat :odds (s/+ odd?) :even (s/? even?)))
 -> :user/oe
 
 (s/conform ::oe [1 3 5 100])
 -> {:odds [1 3 5], :even 100}

Note how the nested s/+ and s/? don’t describe nested collections like [[1 3 5] [100]]. All regex op specs combine to describe the structure of a single top-level collection. This also applies to named regex ops, which allows us to factor regex op specs into smaller reusable pieces:

 (s/def ::odds (s/+ odd?))
 -> :user/odds
 
 (s/def ::optional-even (s/? even?))
 -> :user/optional-even
 
 (s/def ::oe2 (s/cat :odds ::odds :even ::optional-even))
 -> :user/oe2
 
 (s/conform ::oe2 [1 3 5 100])
 -> {:odds [1 3 5], :even 100}

Variable Argument Lists

Consider now how we might spec the arguments for a function that took multiple arguments. For example, println takes zero or more objects and prints them as a string with spaces between them. We can use the any? predicate to specify each object and s/* to indicate the repetition:

 (s/def ::println-args (s/* any?))

We might also have both some fixed arguments and a variable argument at the end. For example, in the clojure.set namespace, the intersection function takes at least one initial set, followed by any number of sets to intersect:

 (doc clojure.set/intersection)
 | -------------------------
 | clojure.set/intersection
 | ([s1] [s1 s2] [s1 s2 & sets])
 | Return a set that is the intersection of the input sets
 -> nil
 
 (clojure.set/intersection #{1 2} #{2 3} #{2 5})
 -> #{2}

We can spec the arguments to intersection as follows.

 (s/def ::intersection-args
  (s/cat :s1 set?
  :sets (s/* set?)))
 
 (s/conform ::intersection-args '[#{1 2} #{2 3} #{2 5}])
 -> {:s1 #{1 2}, :sets [#{3 2} #{2 5}]}

To conform the args, we pass them in a vector, just as if we were invoking apply on the function and passing this vector of args. The conformed value returns the map describing each argument.

In this case, because each argument is the same spec, we could also use just s/+:

 (s/def ::intersection-args-2 (s/+ set?))
 -> :user/intersection-args-2
 
 (s/conform ::intersection-args-2 '[#{1 2} #{2 3} #{2 5}])
 -> [#{1 2} #{3 2} #{2 5}]

Another common case in Clojure is the use of optional keyword arguments. For example, looking at the atom function, it has a signature (atom x & options) with options named :meta or :validator. Clojure supports the destructuring of these keyword options as if they were a map. Clojure spec can also create a regex spec as if these were a map using s/keys*, which has the identical structure to s/keys.

You can spec atom’s args like this (some of these args are deliberately under-specified for demonstration purposes):

 (s/def ::meta map?)
 -> :user/meta
 
 (s/def ::validator ifn?)
 -> :user/validator
 
 (s/def ::atom-args
  (s/cat :x any? :options (s/keys* :opt-un [::meta ::validator])))
 -> :user/atom-args
 (s/conform ::atom-args [100 :meta {:foo 1} :validator int?])
 -> {:x 100,
  :options {:meta {:foo 1},
  :validator #object[clojure.core$int_QMARK_ ...]}}

The atom function follows a typical pattern of having two arities—one with options and one without. This is the most common case for multi-arity functions. However, it’s also typical to encounter functions with an optional first argument, multiple invocation styles, or an argument that is repeated many times. Regex specs can cover all of these cases, and we’ll look at another example in the next section.

Multi-arity Argument Lists

You can see another case of multi-arity argument lists in the repeat function, which has two arities, one with and one without the length n, which is the first argument, not the second. The spec can simply declare that first argument as optional:

 (doc repeat)
 | -------------------------
 | clojure.core/repeat
 | ([x] [n x])
 | Returns a lazy (infinite!, or length n ​if​ supplied) sequence of xs.
 -> nil
 
 (s/def ::repeat-args
  (s/cat :n (s/? int?) :x any?))
 -> :user/repeat-args
 
 (s/conform ::repeat-args [100 ​"foo"​])
 -> {:n 100, :x ​"foo"​}
 
 (s/conform ::repeat-args [​"foo"​])
 -> {:x ​"foo"​}

In some relatively rare cases, the arities are sufficiently different that it makes more sense to use s/alt to fully describe each arity.

Now that we’ve examined how to spec the arguments of a function, it’s time to spec the function itself.

Specifying Functions

Function specs are a combination of three different specs for the arguments, the return value, and the “fn” spec that describes the relationship between the arguments and return.

Let’s start with a function spec for rand:

 clojure.core/rand
 ([] [n])
  Returns a random floating point number between 0 (inclusive) and
  n (default 1) (exclusive).

We can first create an argument spec that is either empty or takes an optional number:

 (s/def ::rand-args (s/cat :n (s/? number?)))

The docstring states the function’s return value is a floating point number, but we can more precisely state that it will be a double:

 (s/def ::rand-ret double?)

We then need to consider the :fn spec, which receives a map containing the conformed args and the conformed return value based on their specs. In this case, the docs state that the random number must be >= 0 and <= n. Let’s state that as a predicate:

 (s/def ::rand-fn
  (​fn​ [{:keys [args ret]}]
  (​let​ [n (or (:n args) 1)]
  (​cond​ (zero? n) (zero? ret)
  (pos? n) (and (>= ret 0) (< ret n))
  (neg? n) (and (<= ret 0) (> ret n))))))

We can now tie all these together using s/fdef, which takes a fully-qualified function name and one or more of the specs (we’ll supply all three):

 (s/fdef clojure.core/rand
  :args ::rand-args
  :ret ::rand-ret
  :fn ::rand-fn)

In a moment we’ll see how to use these specs, but first, let’s take a slight detour to talk about spec’ing anonymous functions.

Anonymous Functions

Higher order functions (ones that take and return functions) are common in Clojure. Use s/fspec to define the spec of an anonymous function. The syntax is the same as s/fdef but omits the function name. For instance, consider a function opposite, which takes a predicate function and creates the opposite predicate function:

 (​defn​ opposite [pred]
  (comp not pred))

The function opposite accepts a predicate function, which we can describe using s/fspec. We can use that function spec in both the :args and :ret spec for opposite.

 (s/def ::pred
  (s/fspec :args (s/cat :x any?)
  :ret boolean?))
 
 (s/fdef opposite
  :args (s/cat :pred ::pred)
  :ret ::pred)

It’s also worth considering a simpler spec for anonymous functions—they don’t always need to be fully spec’d, and sometimes simply using ifn? as the spec is sufficient. Now that we’ve seen how to spec functions, let’s consider what we can do with them.

Instrumenting Functions

During development and testing, we can use instrumentation (stest/instrument) to wrap a function with a version that uses spec to verify that the incoming arguments to a function conform to the function’s spec.

Once you’ve defined a spec for a function with s/fdef, call stest/instrument on the fully qualified function symbol to enable it:

 (require '[clojure.spec.test.alpha :as stest])
 (stest/instrument ​'clojure.core/rand​)
 -> [clojure.core/rand]

You can also use stest/enumerate-namespace to enumerate a collection of all symbols in a namespace to pass to stest/instrument:

 (stest/instrument (stest/enumerate-namespace ​'clojure.core​))
 -> [clojure.core/rand]

Note that instrument returns a collection of all symbols that were successfully instrumented. Check the return value to ensure it contains your intended symbols.

Instrumenting a function replaces its var with a new var that will check args and invoke the old function via its var. In the following, an invalid call to rand triggers an error:

 (rand :boom)
 | ExceptionInfo Call to #​'clojure.core/rand​ did not conform to spec​:
 | In​:​ [0] val​:​ :boom fails spec​:​ :user/rand-args
 | at​:​ [:args :n] predicate​:​ number?
 | :clojure.spec.alpha/args (:boom)
 | :clojure.spec.alpha/failure :instrument
 | :clojure.spec.test.alpha/caller {...}

Here we see an explain failure for the rand args spec. It expected a number? but received the value :boom, clearly not a number.

Instrumentation is not designed for production-time usage (there is an overhead in validating the spec and invoking a secondary var), but instrumentation is a valuable tool for catching errors faster at development or test time.

Let’s move from catching invalid calls to a function to instead using a function’s specs to test the function itself with stest/check.

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

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