Welcome! Please see the About page for a little more info on how this works.

0 votes
in Spec by
*Description*

Suppose we want to traverse a spec that caused some spec error, from the root spec embedded in the explain-data to a leaf of the pred that was the actual cause of the error.

We can usually use {{:path}} info in the explain-data for such a purpose:


user=> (s/explain-data (s/tuple integer? string?) [1 :a])
#:clojure.spec.alpha{:problems
                     ({:path [1], ;; <- indicates the 1st subspec, ie. integer?, was the cause of the error
                       :pred clojure.core/string?,
                       :val :a,
                       :via [],
                       :in [1]}),
                     :spec ...,
                     :value [1 :a]}
user=>


If we traverse the spec tree along the {{:path}}, we can eventually reach the leaf pred that raised the spec error.

In some cases, however, it doesn't hold since some specs such as {{s/merge}}, {{s/and}} and {{s/&}} don't put any clue into {{:path}} that tells which subspec actually raised the error:


user=> (s/explain-data (s/merge (s/map-of integer? string?)
                                (s/coll-of (fn [[k v]] (= (str k) v))))
                       {1 "2"})
#:clojure.spec.alpha{:problems
                     ({:path [], ;; <- doesn't tell us anything at all
                       :pred (fn [[k v]] (= (str k) v)), ;; <- we don't know which subspec this pred occurs in
                       :val [1 "2"],
                       :via [],
                       :in [0]}),
                     :spec ...,
                     :value {1 "2"}}
user=>


To achieve our purpose even in those cases, we have to make a nondeterministic choice: that is, choose a subspec arbitrarily and try traversing it down, and if something is wrong along the way, then backtrack to another subspec and so on.

From my experience that I implemented that backtracking algorithm in a library I'm working on ([repo|https://github.com/athos/spectrace]), I think it's much harder to implement correctly than necessary. In fact, my implementation is probably broken in some corner cases, and I don't even know if it's possible in theory to implement it completely correctly.

*Proposal*

To make it easier to implement the spec traversal, this ticket proposes adding the index into {{:path}} that indicates which subspec raised the spec error for {{s/merge}}, {{s/and}} and {{s/&}}, as follows:


user=> (s/explain-data (s/merge (s/map-of integer? string?)
                                (s/coll-of (fn [[k v]] (= (str k) v))))
                       {1 "2"})
#:clojure.spec.alpha{:problems
                     ({:path [1], ;; <- indicates the 1st subspec, ie. (s/coll-of (fn [[k v]] (= (str k) v))) has the actual cause of the error in it
                       :pred (fn [[k v]] (= (str k) v)),
                       :val [1 "2"],
                       :via [],
                       :in [0]}),
                     :spec ...,
                     :value {1 "2"}}
user=>


The enhancement, though it is indeed a breaking change, should reduce radically the effort needed to write the code traversing specs along the {{:path}}.

1 Answer

0 votes
by
Reference: https://clojure.atlassian.net/browse/CLJ-2180 (reported by sohta)
...