Validating the shape of function arguments and return value with pre/post hooks is a great feature in Clojure/Script, but to get useful context when a validation error occurs, we must manually provide context, in every assert
in every hook. We also have the option of using valid?
and explain
from clojure.spec.alpha
, but the output is not easy to quickly parse. What if we could have a tiny library to abstract the tedious ceremony for getting great errors?
Meet Preo, a tiny library that combines the expressive power of clojure.spec
with the readable errors of Expound
. When I say tiny, that's not an exaggeration - the entire library is 27 lines of code. But having used an earlier version of this library in production at Pitch for a couple of years, I can confidently say that not having to re-type or copy-paste the same boilerplate over and over is a huge improvement.
Imagine we have a function that takes a string s
and will do string-things to it. If it doesn't get a string as input, nested string-function calls will throw errors, and we'll have to traverse the stacktrace to find the point of failure.
If we add a pre-hook with a Preo assertion, our program will throw if an invalid argument is provided and show us a contextually rich error so we can quickly find the problem.
(defn my-string-fn [s]
{:pre [(preo.core/arg! string? s)]}
(subs s 2))
(my-string-fn 123)
; java.lang.AssertionError: Invalid argument: s
; -- Spec failed --------------------
;
; 123
;
; should satisfy
;
; string?
;
; -------------------------
; Detected 1 error
;
; at preo.core$throw_error_BANG_.invokeStatic (core.cljc:7)
; preo.core$throw_error_BANG_.invoke (core.cljc:7)
; preo.example/my-string-fn (example.cljc:0)
The stacktrace is truncated here for readability.
Similarly, we might want to ensure the shape of the return value.
(s/def ::pos-int (s/and integer? pos?))
(defn ret-example [v]
{:post [(p/ret! ::pos-int %)]}
v)
(ret-example "asdf")
; java.lang.AssertionError: Invalid return value
; -- Spec failed --------------------
;
; "asdf"
;
; should satisfy
;
; integer?
;
; -- Relevant specs -------
;
; :preo.example/pos-int:
; (clojure.spec.alpha/and clojure.core/integer? clojure.core/pos?)
;
; -------------------------
; Detected 1 error
;
; at preo.core$throw_error_BANG_.invokeStatic (core.cljc:7)
; preo.core$throw_error_BANG_.invoke (core.cljc:7)
; preo.example/ret-example (example.cljc:2)
Preo takes either predicate functions or keywords resolving to specs, and is written with CLJC to make it work in both Clojure and ClojureScript.
This library is certainly not a ground-breaking discovery, but a useful utility if you like to use pre/post hooks and are tired of manually writing assert messages with enough context.
The code is live on Github, the package is published on Clojars and I'm always eager to hear feedback or ideas. But please note: the tiny scope is a feature, not a bug, and I'm unlikely to accept any patches that increases that scope.
Published: 2025-03-09