speck /spɛk/
- a tiny spot.
Speck is a tiny library for your tiny specs. It allows you to write
concise function specs right inside your defn
s and plays nice with others
because it doesn't introduce any custom defn wrappers. See for yourself:
(defn say-hello
#|[(s/? string?) => string?]
([] (say-hello "world"))
([name] (str "Hello, " name "!")))
Ok, admittedly this is a rather stupid example, so here's one from the official docs instead:
;;; before:
(s/fdef ranged-rand
:args (s/and (s/cat :start int? :end int?)
#(< (:start %) (:end %)))
:ret int?
:fn (s/and #(>= (:ret %) (-> % :args :start))
#(< (:ret %) (-> % :args :end))))
(defn ranged-rand [start end]
(- start (long (rand (- end start)))))
;;; after:
(defn ranged-rand
#|[start :- int?, end :- int? => int?
|- (< start end)
|= (and (>= % start) (< % end))]
[start end]
(- start (long (rand (- end start)))))
Add this to your project.clj
:
[speck "1.1.0"]
After that, you'll need to register a reader tag for speck; you can do it by
creating a file named data_readers.clj
in the root of your classpath (i.e. in
the src
directory) with the following content:
{| speck.v1.core/speck-reader}
Alternatively, you can register it from REPL by executing this code:
(set! *data-readers* (assoc *data-readers* '| #'speck.v1.core/speck-reader))
In order to enable :ret and :fn spec checking (highly recommended) you'll need to add orchestra too:
[orchestra "2018.12.06-2"] ; check its github page for the latest version info
Speck will try to automatically use it instead of vanilla instrumentation when it's available, but to make sure it's being used you can setup it manually:
(ns your.app
(:require [speck.v1.core :as speck]
[orchestra.spec.test :as orchestra]
...))
(alter-var-root #'speck/*auto-define-opts* assoc
:instrument-fn orchestra/instrument)
That's it, you're good to go!
So how does that work?
Just put a #|[...]
form inside your defn where the attr-map
usually goes
(after the name, but before the argument list) and you're done! Under the hood,
this will expand to {:speck (| [...])}
, where |
is the macro that generates
the fspec and attaches it to your function.
The features included are:
- named and unnamed positional args specs
- lightweight syntax for args and fn specs
- arity overloading with separate return and fn specs for each arity
- varargs, keyword args and optional args are supported too
- automatic instrumentation
- specs are automatically redefined on defn's recompilation
Skip to the next section if you want examples. The general syntax looks something like this:
#|[arg-x => ret-1 |- args-expr-1 |= fn-expr-1
arg-x arg-y => ret-2 |- args-expr-2 |= fn-expr-2
...
opts*]
where
-
arg-x
andarg-y
can be either specs or tripletsname :- spec
; if no names are given, default argument names%1
,%2
, etc are used -
_
is used to indicate zero-argument clause, i.e.#|[_ => ret ...]
-
ret-1
andret-2
are ret specs for corresponding arities -
args-expr
s are boolean expressions used to generate :args specs for corresponding arities; arguments are available by name -
fn-expr
s are similar toargs-expr
s, except they generate :fn specs and in addition to the arguments the symbol%
is available which refers to the return value -
opts*
ares/fspec
arguments;:gen
is passed directly and all other opts ares/and
ed to the corresponding specs
You can find some simple testable examples here; for a reference of all/most possible options check out the test suite.
;; basic rule is: input => output
(defn abs
#|[number? => (s/and number? pos?)]
[x] ...)
;; if there are no inputs, use `_`:
(defn pandorandom
#|[_ => any?]
[] ...)
;; you can add names to the arguments, though it is optional:
(defn fraction
#|[numerator :- int?, denominator :- pos? => ratio?]
[num den] ...)
;; specs are always matched with args based on their order (think s/cat):
(defn rotate
#|[direction :- ::vec-2d, angle :- ::radians => ::vec-2d]
[{:keys [x y]} a]
...)
;; different arities can have different ret specs:
(defn map
#|[fn? => ::transducer, fn? (s/+ seqable?) => seq?]
([f] ...)
([f coll & colls] ...))
;; note that you can use `s/?` for optional args:
(defn join
#|[(s/? any?) (s/coll-of any?) => string?]
([coll] ...)
([sep coll] ...))
;; use `s/keys*` for keyword args:
(defn start-server
#|[fn? (s/keys* :opt-un [::host ::port]) => ::server]
[handler & {:keys [host port]}]
...)
;; to check predicates against several args at once, use |- syntax:
(defn interval
#|[start :- number?, end :- number? => ::interval
|- (< start end)]
[a b] ...)
;; note that unnamed args will get default names (%1, %2, ...)
;; this is equivalent to the previous example:
(defn interval
#|[number? number? => ::interval |- (< %1 %2)]
[start end] ...)
;; use |= to check invariants connecting the arguments and the return value;
;; the return value is bound to the `%` symbol:
(defn select-keys
#|[m :- map?, ks :- (s/coll-of any?) => map?
|= (= (set ks) (set (keys %)))]
[m ks]
...)
;; unlike in clojure's anonymous functions, `%` is NOT the same as `%1`!
;; this is equivalent to the previous example (but much more confusing):
(defn select-keys
#|[map?, (s/coll-of any?) => map?
|= (= (set %2) (set (keys %)))]
[m ks]
...)
;; finally, you can directly specify fspec opts, such as :gen...
;; :args, :ret and :fn opts will be added to the corresponding specs via s/and:
(defn frobnicate
#|[x? => foo? ;-> (s/and foo? qux?)
x? y? => baz? ;-> (s/and baz? qux?)
:ret qux?]
...)
;; in fact, you can eschew the speck syntax completely and use vanilla clojure
;; spec syntax if that's your thing:
(defn abs
#|[:args (s/cat :x int?), :ret nat-int?]
...)
;; oh, and by the way, you can entirely bypass using the reader literal; this is
;; more wordy, but can be useful when you need to attach more meta to your fn:
;; (:require [speck.v1.core :as speck :refer [|]])
(defn foo
{:speck (| [...])
:some-other meta}
...)
;; importantly, all of this can be used with any defn-like macro:
(defn foo #|[...])
(defn- foo #|[...])
(defmacro foo #|[...])
(defmulti foo #|[...]) ; but see https://clojure.atlassian.net/browse/CLJ-2450
(defun foo #|[...]) ; from https://github.com/killme2008/defun
(defroutes foo #|[...]) ; not sure why you'd want that... but you get the idea!
When you recompile a specked function, speck will detect that and redefine all specks in the same namespace (including the one you've just recompiled); better granularity cannot be achieved unfortunately, but it works good enough in practice.
To control this behavior (enable\disable it, turn debug printing on and off,
etc) alter the var speck.v1.core/*auto-define-opts*
.
Upon redefining, the affected functions will also be instrumented using the
functions specified under :instrument-fn
in *auto-define-opts*
.
You can manually (re)define the specks by calling define-specs-in-current-ns
:
(speck/define-specs-in-current-ns {:instrument-fn orchestra/instrument})
Set speck.v1.core/*prod-mode*
to a non-nil value in production. This will
effectively remove all the #|[...]
and (| ...)
calls from your code. On
load, *prod-mode*
is set to the value of the environment variable
CLJ_SPECK_PROD_MODE
.
Also, you can change the tagged literal reader from speck-reader
to
speck-reader-bypass
to eliminate all the #|[...]
forms from your code.
I consider v1 API to be pretty much finished, thus no major breaking changes should happen here. Note however that spec itself is in alpha, so when the next version will be released this library might get a breaking v2 release too.
Not yet. Cljs supports static metadata on vars, so the port should be pretty straightforward; I just haven't done it yet.
Default clojure's instrument
only checks args specs
(don't ask me why, I don't know); use orchestra instead.
I guess so... but for a worthy cause though! (Right?) You can always use the
longer syntax {:speck (| [...])}
if you so wish.
My take on this is as follows: custom defn-wrapping macros don't compose, so as clojure ecosystem grows and new feature are developed you might face a situation where you have to choose between two (or more) incompatible defn wrappers. (Also, abstractions that compose poorly are just bad in general.)
On the other hand speck is compatible with any defn-like macro that produces a var and accepts a metadata map. For example, you can use it with defmacro, defmulti or custom 3rd-party macros like e.g. defun.
Also, it's compatible with any libraries that extend your definitions in the same way speck does, although admittedly you would have to use the longer syntax for that:
(defn foo
{:speck (| [...])
:shpec (something (completely different))}
...)
In the end, clojure does provide this elegant extension point via vars+metadata, so why not use that?
Should I abuse this library by writing absurdly huge inline specs and never using vanilla s/fdef
again even where it's more appropriate?
No.
Copyright © 2018-2019 https://github.com/j-cr
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.