Tutorial for typed.clj.spec
Welcome! typed.clj.spec is a library that provides generative-testing for static types.
It’s a lot of fun to play around with, but also packs a punch – enjoy!
Preliminaries
Follow the Quickstart to get a REPL running.
This tutorial also assumes these namespaces are loaded:
(require '[clojure.alpha.spec :as s]
'[clojure.alpha.spec.gen :as gen]
'[typed.clj.spec :as t])
Spec Preliminary: Function specs
Function types in typed.clj.spec are mostly provided by spec’s s/fspec
, so let’s review how to use it.
Say you want to generatively test a function that takes integers and returns strings (this would be written [Int -> Str]
in Typed Clojure syntax). Here’s how you’d do it in spec:
;; 1. define an implementation
(defn f
"Stringifies an integer"
[x] (str x))
;; 2. register its spec
(s/def ::f (s/fspec :args (s/cat :x integer?)
:ret string?))
;; 3. generatively test implementation based on spec
(assert (s/valid? ::f f))
We can even provide our own implementation of f
and peek under the covers to see how it is tested.
(assert (s/valid? ::f (fn [x] (prn "x" x) (str x))))
;"x" -1
;"x" 0
;"x" -1
;"x" -1
;"x" -1
;"x" 0
;"x" -1
;"x" -2
;"x" -4
;"x" -7
;"x" -1
;"x" -2
;"x" 365
;"x" 1
;"x" 5
;"x" -2345
;"x" 122
;"x" 101
;"x" 4082
;"x" 1
;"x" -128352
nil
You can control how many inputs are generated by s/fspec
via s/*fspec-iterations*
.
(assert (binding [s/*fspec-iterations* 2]
(s/valid? ::f (fn [x] (prn "x" x) (str x)))))
;"x" -1
;"x" -1
nil
typed.clj.spec provides other function types, but you probably won’t need them when you’re starting out. We’ll cover them later (see t/fcase
if you’re curious).
Parameteric Polymorphism (“for all” types)
In a similar way that functions abstract over the computation of a set of (return) values, parameteric polymorphism is a type-level abstraction over a set of types.
For example, the runtime specification of clojure.core/identity
is the intersection of the following (infinite number of) rewrite rules of values to themselves:
(identity 1) => 1
(identity 2) => 2
(identity true) => true
(identity false) => false
...
We abstract over these rewrite rules with a function value:
(defn identity [x] x)
A similar situation occurs at the type level. The type of identity
is the intersection of an infinite number of types of the form:
(ann identity [Int -> Int])
(ann identity [Bool -> Bool])
(ann identity [(U Bool Int) -> (U Bool Int)])
...
Parameteric polymorphism (henceforce, just “polymorphism”) enables abstraction over these types.
(ann identity (All [x] [x -> x]))
The previous type (in Typed Clojure syntax) reads: “for all types x, is a function from x to x”.
Like a function, polymorphic types need to be applied (instantiated) to be useful. Let’s see how to write a polymorphic type in typed.clj.spec, and then how to instantiate one.
Writing polymorphic specs
The t/all
macro introduces a polymorphic spec. A polymorphic spec has two components: a binder and a body. The binder represents an ordered list of of type variables which are scoped over the body. Here’s a polymorphic spec for clojure.core/identity
.
(s/def
::identity
(t/all :binder (t/binder :x (t/bind-tv))
:body (s/fspec :args (s/cat :x (t/tv :x))
:ret (t/tv :x))))
Compare this to the same type in Typed Clojure:
(ann identity (All [x] [x -> x]))
Notice that the binder [x]
is replaced with a t/binder
call, and type variable occurrences x
are now (t/tv :x)
. typed.clj.spec uses simple keywords to refer to type variables.
Let’s jump to the fun part: generating example calls! Think of the craziest implementation of identity
you can muster, then throw it against ::identity
.
Here are some to get you started:
(assert (s/valid? ::identity (fn [x] x)))
(assert (s/valid? ::identity #(-> {}
(update % vector %)
(update % peek)
(get %))))
(assert (s/valid? ::identity (comp first
(juxt #(apply % []) identity)
(fn [x]
(constantly x)))))
Here’s my favorite variant. It uses a strongly normalizing (terminating) term that it’s been proven untypable in System F, aka., the second-order lambda calculus.
(defn id [input]
(let [I (fn [a] a)
K (fn [b]
(fn [c]
b))
D (fn [d]
(d d))]
(let [GR ((fn [x]
(fn [y]
((y (x I))
(x K))))
D)]
(GR (fn [_]
(fn [_]
input))))))
GR
was discovered by Giannini and Rocca in the late 80’s. You can read a bit more about it and why it’s relevant to Typed Clojure on p127 of my dissertation, under “Higher-order Control Flow Analysis” – basically, it’s checkable via symbolic execution! On p129 I talk about how I accidentally found how to use GR
as the identity function by playing around a bit with symbolic execution.
Try and verify yourself id
actually implements identity
by using generative testing.
Instantiating polymorphic specs
(s/def
::identity
(t/all :binder (t/binder :x (t/bind-tv))
:body (s/fspec :args (s/cat :x (t/tv :x))
:ret (t/tv :x))))
We can manually instantiate a polymorphic binder using t/inst
. Remember, :x
stands for a spec here, so we provide a substitution from type variable names to specs.
(s/describe (t/inst ::identity {:x integer?}))
;=> (fspec :args (cat :x integer?) :ret integer?)
(s/describe (t/inst ::identity {:x any?}))
;=> (fspec :args (cat :x any?) :ret any?)
t/inst
can also fill in any missing variables in a substitution.
(s/describe (t/inst ::identity {}))
;=> (fspec :args (cat :x any?) :ret any?)
Generation strategy
In the section introducing s/fspec
we already saw one way to peek under the covers of the generating testing strategy. Let’s look from another angle.
One of my favorite features of spec is gen/sample
— if you’re unsure of what a spec generates, just sample it! We can dissect the above spec similarly.
The first interesting part is the :binder
. It expects a substitution that instantiates the type variables, so then it can substitute away the t/tv
forms in the body to create a valid spec.
We can understand the shape of a substitution by calling gen/sample
on a t/binder
.
(-> (t/binder :x (t/bind-tv))
s/gen
gen/sample)
;=> ({:x #{{C/U_080 -15/11,
; 518427724599601687001949897925738120598577465472666077502943101692301721 -!Fy6/uEIZY,
; 0.0013707074005742115 -27/44,
; "Uh" 24/17,
; :Yn0-n:_0 \`}}}
; {:x #{#{()}}}
; {:x #{[-6.0 33267649668820513]}}
; {:x #{{}}}
; {:x #{nil}}
; {:x #{#{HDwv?}}}
; {:x #{[[#{-3.824264209922906E-152
; .Mg-
; j+_
; 4.143384697366724E61
; F?w+8**FG_/w_g?o
; 5/8
; 9100308681477659241077706867786655190902894931345205951770259557960950829084131025622763779598130888851398113843421240836538174787
; "4&8.hyOFGkJRxK`IhN 97d%sC5%+msR$` Z`o+%oF`kwUv("
; -6.792916776722165E-53
; "K{2\\vh!+<]c$}]V).0"}
; :f!+89:_!!hGd]]}}
; {:x #{#{}}}
; {:x #{()}}
; {:x #{{() false}}})
... Tutorial work in progress ...