Skip to content

spec-backed forms of defn/defprotocol/..., aided by metadata

License

Notifications You must be signed in to change notification settings

nedap/speced.def

Repository files navigation

speced.def CircleCI

This library provides spec-backed forms of defn, defprotocol, fn, let etc. using the same exact syntax as clojure.core's.

That way, you can strengthen your defns with custom specs (expressed as metadata), while avoiding the hassle of instrumentation, and gaining some extra benefits, such as better performance, error reporting, etc.

Installation

Coordinates

[com.nedap.staffing-solutions/speced.def "2.1.1"]

Note that self-hosted ClojureScript (e.g. Lumo) is unsupported at the moment.

Production setup

clojure-future-spec incompatibility

If your project happens to depend (directly or transitively; try e.g. lein deps :tree) on clojure-future-spec, this library will fail to load.

You can fix that by adding e.g. :exclusions [clojure-future-spec] to whatever dependency was bringing it in.

You also will need to create an empty clojure.future ns.

Synopsis

With speced.def, one expresses specs via metadata:

(spec/def ::int int?)

;; You can pass functions, keywords, or (primitive) type hints indifferently, as metadata:
(speced/defn ^string? inc-and-serialize [^::int n, ^boolean b]
  (-> n inc str))

...the snippet above compiles to something akin to:

;; note that both preconditions and type hints are emitted, derived from the specs
(defn inc-and-serialize ^String [n, ^boolean b]
  {:pre [(int? n) (boolean? b)]
   :post [(string? %)]}
  (-> n inc str))

Expound is used for error reporting, so the actual emitted code is a bit more substantial.

You can pass specs as part of any nested destructurings.

(speced/defn destructuring-example [{:keys [^string? a] :as ^::thing all}]
  ;; :pre conditions will be emitted for `a` and `all`
  )

speced.def's philosophy is to bypass instrumentation altogether. Clojure's precondition system is simple and reliable, and can be cleanly toggled for dev/prod environments via the clojure.core/*assert* variable.

In a future, we might provide a way to build your own defn, tweaking subjective aspects like instrumentation, while preserving all other features at no cost.

Highlights

  • 1:1 syntax mapping to clojure.core's
    • No IDE pains, nothing new to learn, trivial upgradeability from old defns
    • Things like N-arities are supported.
  • Multiple metadata-based 'syntaxes'
  • Type hints become specs
    • e.g.^Boolean x is analog to ^boolean? x
    • emits safe and efficient code, addressing an old complaint about Clojure's type hinting mechanism ("it doesn't enforce types").
    • You can ^::speced/nilable ^Boolean x if something can be nil.
  • Inline function specs can become type hints
    • e.g. ^string? will emit a ^String type hint
    • same for ClojureScript: ^string? -> ^string
    • This particularly matters in JVM Clojure. Refer to the full mapping.
  • speced/defprotocol, speced/fn, speced/let, speced/letfn are also offered
    • Same syntax and advantages as speced/defn
  • Richer Expound integration
    • This idea is implemented here inline, as long as the Expound issue remains open.
  • Full ClojureScript support
    • Did you know Clojure and ClojureScript expect type hinting metadata at different positions?
      • speced.def abstracts over that, allowing you to write them wherever you please.
  • speced/def-with-doc: clojure.spec.alpha/def with a docstring
    • It will show up in speced/doc, along with the spec itself
    • It also will show up in REBL.

Status and roadmap

  • Battle-tested across a variety of projects by now
    • Rough edges polished at this point, each bugfix being thoroughly unit-tested.
    • ClojureScript support is full (and fully unit-tested), but less battle-tested.
  • Richer features will be added
    • Refer to the issue tracker for getting an idea of the project's wishlist.

ns organisation

There are exactly 2 namespaces meant for public consumption:

  • nedap.speced.def: 'speced' forms of defprotocol, defn, fn, let, letfn, etc.
  • nedap.speced.def.doc: a public docstring registry for specs. Can be queried from arbitrary tools, and particularly REBL.

They are deliberately thin so you can browse them comfortably.

Documentation

Please browse the public namespaces, which are documented, speced and tested.

It is part of this library's philosophy to avoid creating external documentation. We believe specs, particularly with our def-with-doc helper, can suffice, preventing duplication/drift and non-speced/non-self-documenting code.

That assumes that adopters are willing to jump to the source (particularly to the public parts, and occasionally to the tests which serve as a large corpus of examples), and preferrably have handy tooling that is able to do such jumping.

Testing

Occasionally, you might want to test that invoking certain speced/ functionality throws a certain spec failure.

For that, clojure.test/is/cljs.test/is are extended with the spec-assertion-thrown? assertion. You can find example usages in our unit.nedap.speced.def.spec-assertion testing ns.

Note that clojure.test cannot be extended with complete cleanliness, so in a way requiring speced.def pollutes the global environment with the spec-assertion-thrown? assertion.

In a future, we might add a way to disable this side-effect.

License

Copyright © Nedap This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0