Skip to content

Instantly share code, notes, and snippets.

@olivergeorge
Last active December 23, 2016 22:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save olivergeorge/85c2b263227676324275ce13408c0fb8 to your computer and use it in GitHub Desktop.
Save olivergeorge/85c2b263227676324275ce13408c0fb8 to your computer and use it in GitHub Desktop.

Context:

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:

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

Weaknesses:

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.

Implementation:

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

@olivergeorge
Copy link
Author

Thanks @luxbock I'll think about this over the break

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment