Currently writing a function spec is a bit like verbose destructing - it can require as much code as the function itself.
Reduced effort in spec'ing functions would be nice.
Spec encourages namespaced keywords to have one "type". That means within your program any map with that key can be assumed to have a predictable type.
A similar convention is to have one meaning for a variable name in a specific namespace. Nothing in spec about that... it's just less confusing.
With those reference points, here's the genius idea:
Use the arglist to generate a function spec. It gets more interesting if we translate arglist destructing into specs too.
e.g. That means (defn x [a b c])
will check:
- [0] conforms to
::a
- [1] conforms to
::b
- [2] conforms to
::c
e.g. That means (defn x [{:keys [url x] :or {x 1} [_ request limit count]])
will check:
- [0] conforms to
(s/keys :req-un [::url] :opt-un [::x])
- [1] conforms to
coll?
- [1 1] conforms to
::request
- [1 2] conforms to
::limit
- [1 3] conforms to
::count
Destructing keys involves a lot of nil punning. e.g. if key is missing, it's okay. We need to pick between using :opt-un or :req-un. Simple unambiguous logic would be:
- If there's an :or clause we use :opt-un
- Else it is a required key and we use :req-un.
Perhaps the key spec can guide this. A more flexible/relaxed approach would be "if the spec is s/nilable then we could use :opt-un and know our fn is getting predictable arg data". That wouldn't pick up expected but missing keys so let's be conservative for now and consider relaxing this later.
The function var has :arglists metadata. That would allow functions to be spec'd without replacing the defn macro.
(defn myfn [a & bs] a)
=> #'cljs.user/myfn
(:arglists (meta (var myfn)))
=> ([a & bs])
Ultimately, you might typically put specs at the top of each namespace like this...
(ns example ...)
(s/def ::base-url string?)
(s/def ::request (s/keys :req-un [::url ::params]))
(s/def ::page-limit pos-int?)
(s/def ::page-count pos-int?)
(defn load-options
[{:keys [base-url]} [_ request page-limit page-count]]
...)
; Instrumenting these might look like this
(instrument-from-arglists *ns*)
In 1.9, clojure.core.specs includes an ::arg-list spec which includes destructuring in the binding-form. That should ease parsing arg-lists into a useful AST to transform.
https://github.com/clojure/clojure/blob/master/src/clj/clojure/core/specs.clj#L67
Thanks @luxbock I'll think about this over the break