“Ralph made a step forward and Jack smacked Piggy’s head. Piggy’s glasses flew off and tinkled on the rocks. Piggy cried out in terror: ‘My specs! One side’s broken.”
— William Golding, Lord of the Flies, Chapter 4
piggy is a Clojure library that helps with broken
specs.
Will be in alpha as long as clojure.spec is in alpha.
Download from https://github.com/sgepigon/piggy.
Let's say we're writing a spec for clojure.core/+. We might start out
like this:
(ns piggy.demo.readme
(:require
[clojure.spec.alpha :as s]
[piggy.combinators.alpha :as pc]))
(+ 2 2)
;; => 4
(s/fspec :args (s/cat :x int? :y int?)
:ret int?)
;; `s/fspec` returns the function itself on a successful conform
(s/conform (s/fspec :args (s/cat :x int? :y int?)
:ret int?)
+)
;; => #function[clojure.core/+]
;; Can also use `s/explain` to print a human readable message:
(s/explain (s/fspec :args (s/cat :x int? :y int?)
:ret int?)
+)
;; => Success!We think about it a little more and realize + is variadic:
(+) ; => 0
(+ 1) ; => 1
(+ 0 1 2 3 4 5 6 7 8 9) ; => 45We can use s/* for zero or more ints.
(s/explain (s/fspec :args (s/* int?)
:ret int?)
+)
;; => (-8782045379102980082 -441326657751795727) - failed: integer overflowTwo things are going on here:
-
Because we switch from a 2-arity—
(s/cat :x int? y: int?)—to variadic—(s/* int?)—we're hitting integer overflow. -
Turns out
clojure.core/+doesn't auto-promote. But there is a built-in Clojure function,clojure.core/+', that does arbitrary precision.So let's try this with
+'.
(s/explain (s/fspec :args (s/* int?)
:ret int?)
+')
;; => 9223372036854775808N - failed: int? at: [:ret]We can see it auto-promotes but now our return spec is wrong. We also
realize +' takes all sorts of numbers.
(+ -1 3.14 22/7 0x77)
;; => 124.28285714285714Let's update int? to number?
(s/explain (s/fspec :args (s/* number?)
:ret number?)
+')
;; => Success!Cool, we're more comfortable with this spec. How do we know change isn't a breaking change?
compat and fcompat are spec combinators that encodes a compatibility
property between two specs.
compat is for simple comparsions over specs and predicates while
fcompat is used to compare compatibility between functions. s/spec
is to compat as s/fspec is to fcompat.
The combinators are is a spec themselves, so the same functions that
work on clojure.spec specs (s/conform, s/unform, s/explain,
s/gen, s/with-gen, and s/describe) work on compat and fcompat.
(s/explain (pc/fcompat :old (s/fspec :args (s/cat :x int? :y int?)
:ret int?)
:new (s/fspec :args (s/* number?)
:ret number?))
+')
;; => 1.0 - failed: int? at: [:ret :old]We see here it's a breaking change: we promised an int? but now we're
saying we're returning a number?—this is weakening a promise.
We can show it trivially with clojure.core/any?
(s/explain (pc/fcompat :old (s/fspec :args (s/* number?)
:ret number?)
:new (s/fspec :args (s/* number?)
:ret any?))
+')
;; => \space - failed: clojure.core/number?: any? is less constrained than
;; number? at: [:ret :old]Similarlity, we can show a breaking change by requiring more in the arguments:
(s/explain (pc/fcompat :old (s/fspec :args (s/* number?)
:ret number?)
:new (s/fspec :args (s/cat :x number? :y number?)
:ret number?))
+')
;; => () - failed: Insufficient input at: [:args :new :x]Run Clojure unit tests with either:
lein test
or
clj -A:test
Copyright © 2018–2019 Santiago Gepigon III
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.