Skip to content

Differences from spec.alpha

Alex Miller edited this page Feb 8, 2019 · 20 revisions

A summary of differences between spec.alpha and spec-alpha2. Everything is subject to change as spec-alpha2 is a work in progress.

Symbolic specs

spec 1 defined a language of spec forms (like s/and) to define specs. These specs were implemented as macros taking both evaluated objects (symbols or functions) and nested spec forms. The api call s/form would produce a symbolic spec from a spec object. One downside of this approach is that the reliance on macros made it challenging to programmatically construct spec forms without using eval or additional macros.

In spec 2, we are more strictly separating the worlds of symbolic specs and spec objects. Symbolic specs consist only of:

  • Spec forms (lists/seqs), with spec op in function position, composed of other symbolic specs
  • Qualified keywords (names that can be looked up in the registry)
  • Qualified symbols (predicate function references)
  • Sets of constant values (an enumeration)

The function s/spec* takes symbolic specs and returns spec objects (extensions of the Spec protocol) for runtime use. s/form is the inverse operation, which takes a spec object and returns a symbolic spec.

The spec forms are themselves macros, which expand to a call to spec* on themselves. The only modification made is that spec form macros accept symbols that are not fully-qualified and will qualify ("explicate") them in the namespace context where they are invoked (at compile-time). This is a convenience for people writing spec forms.

The spec registry is a stateful runtime construct that provides a mapping from spec names (either qualified keywords or qualified symbols) to spec objects. A new function s/register has been added that adds a mapping from name to spec object. s/def is a helpful wrapper macro for s/register that understands how to interpret all kinds of symbolic specs (not just spec forms), including symbols and sets, which will be wrapped in the helper s/spec spec op.

The spec API functions (valid?, conform, explain, etc) now accept only keywords (spec names), and spec objects. Spec 1 accepted things like function objects (evaluated symbols or anonymous functions) as well - those now must be explicitly wrapped in s/spec forms.

Nested regex contexts

Regex specs (cat, alt, *, +, ?, etc) combine to describe a single sequential collection. If the collection contains a nested collection, something needs to be inserted to prevent that combination. In spec 1, this was done with the s/spec macro. In spec 2, a new s/nest operation serves this purpose and is used only for this.

Creating Specs Programmatically

The new s/spec* functional entry point can be used to construct spec objects from spec forms without invoking the spec op macros or using eval:

(require '[clojure.spec-alpha2 :as s])

(defn or-of
  "Make or spec where tags match names"
  [& spec-names]
  (let [tag-names (->> spec-names (map name) (map keyword))]
    (s/spec* (cons `s/or (interleave tag-names spec-names)))))

(s/def ::a int?)
(s/def ::b keyword?)
(s/conform (or-of ::a ::b) 100)
;; [:a 100]

Implementing Custom Specs

Simple spec ops

It's common to need a parameterized custom spec op and these can now easily be created with s/defop. Ops created with s/defop are defined by a parameterized specs, have the form you'd expect, and can optionally provide a custom generator (otherwise will use gen from the spec provided). They conform/unform based on the spec definition.

(s/defop bounded-string
  "Specs a string with bounded size."
  [min max]
  :gen #(gen/fmap str (s/gen (s/int-in 10000 100000)))
  (s/and string? #(<= min (count %) max)))

user=> (s/def ::zip (bounded-string 5 9))
:user/zip
user=> (s/form ::zip)
(user/bounded-string 5 9)
user=> (s/conform ::zip "90210")
"90210"
user=> (s/explain ::zip "9021")
"9021" - failed: (<= 5 (count %) 9) spec: :user/zip
user=> (gen/sample (s/gen ::zip))
("10000" "10001" "10000" "10001" "10002" "10015" "10017" "10005" "10003" "10000")

Full spec ops

Custom spec ops can now be created and installed with a two-step process. First, create a spec op macro that explicates (fully-qualifies) a form and invokes the functional interface:

(defmacro my-spec
  [& opts]
  `(s/spec* '~(s/explicate (ns-name *ns*) `(my-spec ~@opts))))

Second, calls to spec* get routed (via the spec name) to the multimethod create-spec that actually creates the spec by reifying the Spec protocol:

(defmethod create-spec 'my-ns/my-spec
  [[_ & opts]]
  (reify Spec
    ...))

Clojure integration

Because Clojure itself does not know about spec-alpha2, certain integration features will not work as expected (doc won't see the registry, error stack reporting may not print the correct failure location during instrumentation, and macros will not be automatically checked).

Clone this wiki locally