From Clojure 1.9 and up, the core language is guarded by various clojure.spec definitions. This means, even if you’re not using spec directly yourself, you will potentially encounter spec derived errors.
These errors, much like all errors in Clojure, are extremely verbose and make it hard to discern what the actual problem is at a glance. These errors are great for machines but not so great for humans, who happen to be the primary consumer of these errors. I consider this to be a bit of a design flaw within spec, maybe this will improve over time.
Until such potential improvements are implemented, thanks to the power and wonders of Clojure, we can use a library called expound to make our lives easier. This tool is inspired by Elm and it shows. Without expound and this incorrect syntax:
1 2 |
(let [{:a b} {:a 10}] b) |
We get this beauty of an error from Clojure itself:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
CompilerException clojure.lang.ExceptionInfo: Call to clojure.core/let did not conform to spec: In: [0 0 0 0] val: :a fails spec: :clojure.core.specs.alpha/local-name at: [:args :bindings :binding :map :mb 0 :sym] predicate: simple-symbol? In: [0 0 0 0] val: :a fails spec: :clojure.core.specs.alpha/seq-binding-form at: [:args :bindings :binding :map :mb 0 :seq] predicate: vector? In: [0 0 0 0] val: :a fails spec: :clojure.core.specs.alpha/map-bindings at: [:args :bindings :binding :map :mb 0 :map] predicate: coll? In: [0 0 0 0] val: :a fails spec: :clojure.core.specs.alpha/map-special-binding at: [:args :bindings :binding :map :mb 0 :map] predicate: map? In: [0 0 0 0] val: :a fails spec: :clojure.core.specs.alpha/ns-keys at: [:args :bindings :binding :map :nsk 0] predicate: qualified-keyword? In: [0 0 0 1] val: b fails spec: :clojure.core.specs.alpha/ns-keys at: [:args :bindings :binding :map :nsk 1] predicate: vector? In: [0 0 0 0] val: :a fails spec: :clojure.core.specs.alpha/map-bindings at: [:args :bindings :binding :map :msb 0] predicate: #{:as :or :syms :keys :strs} In: [0 0] val: {:a b} fails spec: :clojure.core.specs.alpha/local-name at: [:args :bindings :binding :sym] predicate: simple-symbol? In: [0 0] val: {:a b} fails spec: :clojure.core.specs.alpha/seq-binding-form at: [:args :bindings :binding :seq] predicate: vector? :clojure.spec.alpha/spec #object[clojure.spec.alpha$regex_spec_impl$reify__1200 0x6ef7c8e9 "[email protected]"] :clojure.spec.alpha/value ([{:a b} {:a 10}] b) :clojure.spec.alpha/args ([{:a b} {:a 10}] b) #:clojure.spec.alpha{:problems ({:path [:args :bindings :binding :sym], :pred clojure.core/simple-symbol?, :val {:a b}, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/local-name], :in [0 0]} {:path [:args :bindings :binding :seq], :pred clojure.core/vector?, :val {:a b}, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/seq-binding-form], :in [0 0]} {:path [:args :bindings :binding :map :mb 0 :sym], :pred clojure.core/simple-symbol?, :val :a, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/local-name], :in [0 0 0 0]} {:path [:args :bindings :binding :map :mb 0 :seq], :pred clojure.core/vector?, :val :a, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/seq-binding-form], :in [0 0 0 0]} {:path [:args :bindings :binding :map :mb 0 :map], :pred clojure.core/coll?, :val :a, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings], :in [0 0 0 0]} {:path [:args :bindings :binding :map :mb 0 :map], :pred map?, :val :a, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-special-binding], :in [0 0 0 0]} {:path [:args :bindings :binding :map :nsk 0], :pred clojure.core/qualified-keyword?, :val :a, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/ns-keys], :in [0 0 0 0]} {:path [:args :bindings :binding :map :nsk 1], :pred clojure.core/vector?, :val b, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/ns-keys], :in [0 0 0 1]} {:path [:args :bindings :binding :map :msb 0], :pred #{:as :or :syms :keys :strs}, :val :a, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings], :in [0 0 0 0]}), :spec #object[clojure.spec.alpha$regex_spec_impl$reify__1200 0x6ef7c8e9 "[email protected]"], :value ([{:a b} {:a 10}] b), :args ([{:a b} {:a 10}] b)}, compiling:(/home/ollie/code.clj:20:1) |
Can you tell what’s going on? Probably after a little bit of time if you know what you’re looking for. Imagine this in a much more complex case though, let’s say within a few layers of macros. Now here’s the same response through expound:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
CompilerException clojure.lang.ExceptionInfo: Call to clojure.core/let did not conform to spec: -- Spec failed -------------------- ([[:a ...] ...] ...) ^^ should satisfy simple-symbol? or vector? or coll? or map? or qualified-keyword? -- Relevant specs ------- :clojure.core.specs.alpha/ns-keys: (clojure.spec.alpha/tuple (clojure.spec.alpha/and clojure.core/qualified-keyword? (fn* [p1__45#] (clojure.core/-> p1__45# clojure.core/name #{"syms" "keys"}))) (clojure.spec.alpha/coll-of clojure.core/simple-symbol? :kind clojure.core/vector?)) :clojure.core.specs.alpha/map-special-binding: (clojure.spec.alpha/keys :opt-un [:clojure.core.specs.alpha/as :clojure.core.specs.alpha/or :clojure.core.specs.alpha/keys :clojure.core.specs.alpha/syms :clojure.core.specs.alpha/strs]) :clojure.core.specs.alpha/seq-binding-form: (clojure.spec.alpha/and clojure.core/vector? (clojure.spec.alpha/cat :elems (clojure.spec.alpha/* :clojure.core.specs.alpha/binding-form) :rest (clojure.spec.alpha/? (clojure.spec.alpha/cat :amp #{'&} :form :clojure.core.specs.alpha/binding-form)) :as (clojure.spec.alpha/? (clojure.spec.alpha/cat :as #{:as} :sym :clojure.core.specs.alpha/local-name)))) :clojure.core.specs.alpha/local-name: (clojure.spec.alpha/and clojure.core/simple-symbol? (clojure.core/fn [%] (clojure.core/not= '& %))) :clojure.core.specs.alpha/map-binding: (clojure.spec.alpha/tuple :clojure.core.specs.alpha/binding-form clojure.core/any?) :clojure.core.specs.alpha/map-bindings: (clojure.spec.alpha/every (clojure.spec.alpha/or :mb :clojure.core.specs.alpha/map-binding :nsk :clojure.core.specs.alpha/ns-keys :msb (clojure.spec.alpha/tuple #{:as :or :syms :keys :strs} clojure.core/any?)) :into {}) :clojure.core.specs.alpha/map-binding-form: (clojure.spec.alpha/merge :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-special-binding) :clojure.core.specs.alpha/binding-form: (clojure.spec.alpha/or :sym :clojure.core.specs.alpha/local-name :seq :clojure.core.specs.alpha/seq-binding-form :map :clojure.core.specs.alpha/map-binding-form) :clojure.core.specs.alpha/binding: (clojure.spec.alpha/cat :binding :clojure.core.specs.alpha/binding-form :init-expr clojure.core/any?) :clojure.core.specs.alpha/bindings: (clojure.spec.alpha/and clojure.core/vector? (clojure.spec.alpha/* :clojure.core.specs.alpha/binding)) -- Spec failed -------------------- ([[... b] ...] ...) ^ should satisfy vector? -- Relevant specs ------- :clojure.core.specs.alpha/ns-keys: (clojure.spec.alpha/tuple (clojure.spec.alpha/and clojure.core/qualified-keyword? (fn* [p1__45#] (clojure.core/-> p1__45# clojure.core/name #{"syms" "keys"}))) (clojure.spec.alpha/coll-of clojure.core/simple-symbol? :kind clojure.core/vector?)) :clojure.core.specs.alpha/map-bindings: (clojure.spec.alpha/every (clojure.spec.alpha/or :mb :clojure.core.specs.alpha/map-binding :nsk :clojure.core.specs.alpha/ns-keys :msb (clojure.spec.alpha/tuple #{:as :or :syms :keys :strs} clojure.core/any?)) :into {}) :clojure.core.specs.alpha/map-binding-form: (clojure.spec.alpha/merge :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-special-binding) :clojure.core.specs.alpha/binding-form: (clojure.spec.alpha/or :sym :clojure.core.specs.alpha/local-name :seq :clojure.core.specs.alpha/seq-binding-form :map :clojure.core.specs.alpha/map-binding-form) :clojure.core.specs.alpha/binding: (clojure.spec.alpha/cat :binding :clojure.core.specs.alpha/binding-form :init-expr clojure.core/any?) :clojure.core.specs.alpha/bindings: (clojure.spec.alpha/and clojure.core/vector? (clojure.spec.alpha/* :clojure.core.specs.alpha/binding)) -- Spec failed -------------------- ([[:a ...] ...] ...) ^^ should be one of: `:as`,`:or`,`:syms`,`:keys`,`:strs` -- Relevant specs ------- :clojure.core.specs.alpha/map-bindings: (clojure.spec.alpha/every (clojure.spec.alpha/or :mb :clojure.core.specs.alpha/map-binding :nsk :clojure.core.specs.alpha/ns-keys :msb (clojure.spec.alpha/tuple #{:as :or :syms :keys :strs} clojure.core/any?)) :into {}) :clojure.core.specs.alpha/map-binding-form: (clojure.spec.alpha/merge :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-special-binding) :clojure.core.specs.alpha/binding-form: (clojure.spec.alpha/or :sym :clojure.core.specs.alpha/local-name :seq :clojure.core.specs.alpha/seq-binding-form :map :clojure.core.specs.alpha/map-binding-form) :clojure.core.specs.alpha/binding: (clojure.spec.alpha/cat :binding :clojure.core.specs.alpha/binding-form :init-expr clojure.core/any?) :clojure.core.specs.alpha/bindings: (clojure.spec.alpha/and clojure.core/vector? (clojure.spec.alpha/* :clojure.core.specs.alpha/binding)) ------------------------- Detected 3 errors #:clojure.spec.alpha{:problems ({:path [:args :bindings :binding :sym], :pred clojure.core/simple-symbol?, :val {:a b}, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/local-name], :in [0 0]} {:path [:args :bindings :binding :seq], :pred clojure.core/vector?, :val {:a b}, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/seq-binding-form], :in [0 0]} {:path [:args :bindings :binding :map :mb 0 :sym], :pred clojure.core/simple-symbol?, :val :a, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/local-name], :in [0 0 0 0]} {:path [:args :bindings :binding :map :mb 0 :seq], :pred clojure.core/vector?, :val :a, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/seq-binding-form], :in [0 0 0 0]} {:path [:args :bindings :binding :map :mb 0 :map], :pred clojure.core/coll?, :val :a, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings], :in [0 0 0 0]} {:path [:args :bindings :binding :map :mb 0 :map], :pred map?, :val :a, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-special-binding], :in [0 0 0 0]} {:path [:args :bindings :binding :map :nsk 0], :pred clojure.core/qualified-keyword?, :val :a, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/ns-keys], :in [0 0 0 0]} {:path [:args :bindings :binding :map :nsk 1], :pred clojure.core/vector?, :val b, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/ns-keys], :in [0 0 0 1]} {:path [:args :bindings :binding :map :msb 0], :pred #{:as :or :syms :keys :strs}, :val :a, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings], :in [0 0 0 0]}), :spec #object[clojure.spec.alpha$regex_spec_impl$reify__1200 0x54ba9618 "[email protected]"], :value ([{:a b} {:a 10}] b), :args ([{:a b} {:a 10}] b)}, compiling:(/home/ollie/code.clj:20:1) |
That’s much better! Suddenly you don’t have to run a REPL in your brain to understand what went wrong, the machine is telling you exactly what is wrong, where and what you can do instead. It may not be as succinct as Elm, but the information at the start is just as useful.
Integration
Luckily, expound happens to be extremely easy to use. Hopefully we can make that easier by including it by default in a lot of beginner friendly code too. The README does a great job of explaining how to use it.
Replace calls to
clojure.spec.alpha/explain
withexpound.alpha/expound
and toclojure.spec.alpha/explain-str
withexpound.alpha/expound-str
.
If you don’t use explain directly and you’d like all spec errors to be run through expound (including those from Clojure the language), then you can hook it in globally like the following snippet. I’d recommend running this within your (ns user) before your REPL loads or in the main ns of your application before it starts up.
1 2 3 4 5 6 7 8 9 |
(ns user (:require [clojure.spec.alpha :as s] [expound.alpha :as expound])) ;; If you're doing this in the user ns for your repl... (alter-var-root #'s/*explain-out* (constantly expound/printer)) ;; Otherwise, you can use the method from the README. (set! s/*explain-out* expound/printer) |
Now any spec error generated from here on out will be formatted for human consumption by expound, excellent!
I doubt I’m alone in thinking that I’d love this to be the default within Clojure or at least extremely widespread in it’s usage. Much like figwheel for ClojureScript projects, we would always use expound alongside our specs. Maybe CIDER could be a good entry point for this addition.