gradual
Gradual, dependent typing for Clojure(Script). Also comes with clojure.spec
interop (see gradual.spec
).
The successor of alexandergunnarson/defnt, which is too narrowly named.
defn-spec
was developed independently, but surprisingly yields some very similar ideas with respect to gradual.spec/defn
(see below) and even a similar interface for specs on function arguments.gradual.typed
's idea of performing spec conformance checks at compile time (though it aims exclusively for "there-exists" checks via generative testing; gradual.type
also provides as many compile-time "for-all" proofs as it can before falling back to compile-time generative testing and/or runtime spec checks).gradual.type
)(Work in progress — check back later.)
gradual.spec
)Fundamentally, gradual.spec/defn
provides spec information in context rather than drastically increasing cognitive overhead and code duplication via s/fdef
-style decontextualization. (There are other features and benefits that will be approached as development progresses.)
clojure.spec
is a great leap forward for Clojure. It is difficult to overstate the value that it delivers via the expressive and composable data specifications it supports, and the corresponding generative tests they in turn yield out of the box. But, as ever, there is room for improvement. It is a perfectly defensible design decision for the creators of clojure.spec
to have decoupled specs from the implementation they specify. However, the position gradual.spec/defn
takes is that there is much greater value in colocating the spec with the spec'ed. This approach yields the following benefits:
fdef
+ defn
At the moment, it has only been tested with Clojure 1.8 and 1.9, but it should be trivial to test with ClojureScript in a coming release, as there is no platform-specific code.
To take a reasonably simple example:
(require
'[[clojure.spec.alpha :as s]
[gradual.spec :as gs]])
(gs/defn abc
[a pos-int?, b (s/and double? #(> % 3))
| (> b a)
> (s/and map? #(= (:a %) a))]
(sorted-map :a a :b (+ b 8)))
Deconstructed, the above code defines a function abc
with only one overload, such that:
a
and b
a
must satisfy pos-int?
b
must satisfy (s/and double? #(> % 3))
(> b a)
must hold true
|
defines an overload's precondition similarly to :pre
(but implementationally the precondition becomes part of fdef
and does not use :pre
in any way)(s/and map? #(= (:a %) a))
>
defines an overload's postcondition similarly to :post
(but like |
, implementationally the precondition becomes part of fdef
and does not use :post
in any way)The above gs/defn
code generates the following:
(s/fdef abc
:args (s/or :arity-2
(s/and
(s/cat
:a pos-int?
:b (s/and double? (fn* [p1__3954#] (> p1__3954# 3))))
(fn [{a :a, b :b}] (> b a))))
:fn (gradual.spec.util/with-gen-spec
(fn [{ret# :ret}] ret#)
(fn [{[arity-kind# args#] :args}]
(case arity-kind#
:arity-2
(let [{a :a, b :b} args#]
(s/spec (s/and map? #(= (:a %) a))))))))
(defn abc [a b] (sorted-map :a a :b (+ b 8)))
where gs.util/with-gen-spec
is a low-complexity, few-LOC macro that assists in spec auditability and data flow.
Note that spec'ing destructurings is also possible. Take the more complex example below:
(gs/defn abcde "Documentation" {:whatever-metadata "fhjik"}
([a number? > number?] (inc a))
([a pos-int?, b pos-int?
| (> a b)
> (s/and number? #(> % a) #(> % b))] (+ a b))
([a #{"a" "b" "c"}
b boolean?
{:as c
:keys [ca keyword? cb string?]
{:as cc
{:as cca
:keys [ccaa keyword?]
[[ccabaa some? {:as ccabab :keys [ccababa some?]} some?] some? ccabb some? & ccabc some? :as ccab]
[:ccab seq?]}
[:cca map?]}
[:cc map?]}
#(-> % count (= 3))
[da double? & db seq? :as d] sequential?
[ea symbol?] ^:gen? (s/coll-of symbol? :kind vector?)
& [fa #{"a" "b" "c"} :as f] seq?
| (and (> da 50) (contains? c a)
a b c ca cb cc cca ccaa ccab ccabaa ccabab ccababa ccabb ccabc d da db ea f fa)
> number?] 0))
which expands to:
(s/fdef abcde
:args
(s/or
:arity-1 (s/cat :a number?)
:arity-2 (s/and (s/cat :a pos-int?
:b pos-int?)
(fn [{a :a b :b}] (> a b)))
:arity-varargs
(s/and
(s/cat
:a #{"a" "b" "c"}
:b boolean?
:c (gs/map-destructure #(-> % count (= 3))
{:ca keyword?
:cb string?
:cc (gs/map-destructure map?
{:cca
(gs/map-destructure map?
{:ccaa keyword?
:ccab
(gs/seq-destructure seq?
[:arg-0 (gs/seq-destructure some?
[:ccabaa some?
:ccabab (gs/map-destructure some? {:ccababa some?})])
:ccabb some?]
[:ccabc some?])})})})
:d (gs/seq-destructure sequential? [:da double?] [:db seq?])
:arg-4# (gs/seq-destructure ^{:gen? true} (s/coll-of symbol? :kind vector?) [:ea symbol?])
:f (gs/seq-destructure seq? [:fa #{"a" "b" "c"}]))
(fn [{a :a
b :b
{:as c
:keys [ca cb]
{:as cc
{:as cca
:keys [ccaa]
[[ccabaa {:as ccabab :keys [ccababa]}] ccabb & ccabc :as ccab] :ccab} :cca} :cc} :c
[da & db :as d] :d
[ea] :arg-4#
[fa :as f] :f :as X}]
(and (> da 50) (= a fa)
a b c ca cb cc cca ccaa ccab ccabaa ccabab ccababa ccabb ccabc d da db ea f fa))))
:fn
(gradual.spec.util/with-gen-spec (fn [{ret# :ret}] ret#)
(fn [{[arity-kind# args#] :args}]
(case arity-kind#
:arity-1
(let [{a :a} args#] (s/spec number?))
:arity-2
(let [{a :a b :b} args#] (s/spec (s/and number? #(> % a) #(> % b))))
:arity-varargs
(let [{a :a
b :b
{:as c
:keys [ca cb]
{:as cc
{:as cca
:keys [ccaa]
[[ccabaa {:as ccabab :keys [ccababa]}] ccabb & ccabc :as ccab] :ccab} :cca} :cc} :c
[da & db :as d] :d
[ea] :arg-4#
[fa :as f] :f} args#] (s/spec number?))))))
(defn abcde
"Documentation"
{:whatever-metadata "fhjik"}
([a] (inc a))
([a b] (+ a b))
([a b
{:as c
:keys [ca cb]
{:as cc
{:as cca
:keys [ccaa]
[[ccabaa {:as ccabab :keys [ccababa]}] ccabb :as ccab] :ccab} :cca} :cc}
[da & db :as d]
[ea]
&
[fa :as f]]
0))
(gs/defn abc [{:keys [a a-conformer, b b-conformer] :as c} c-conformer] ...)
where a
is conformed, b
is conformed, and c
is conformed separately such that (not= a (first c))
:
c
is conformed first, then from it is destructured a
and b
a
and b
are conformeda
and b
are respectively associated into c
if their conformed values are non-identical
core/map?
c
is intentionally not re-conformed(gs/defn abc [[a a-conformer, b b-conformer :as c] c-conformer] ...)
where a
is conformed, b
is conformed, and c
is conformed separately such that (not= a (first c))
:
c
is conformed first, then from it is destructured a
and b
a
and b
are conformeda
and b
are respectively concat
'ed onto the rest of c
(a later optimization will be that
this is done only if their conformed values are non-identical). Thus c
will always end up
being a seq.
core/seqable?
c
is intentionally not re-conformedCopyright © 2020 Alex Gunnarson
Distributed under the Creative Commons Attribution-ShareAlike 3.0 US (CC-SA) license.
For normal people who don't speak legalese, this means:
But:
Pretty easy, common-sense, decent stuff! Thanks :)
For more information, see tldrlegal's summary of the CC-SA license.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close