(pred f) predicate wrapperThe pred wrapper provides an unambiguous predicate escape hatch at the value
level. Wrap a 1-arg function with pred to match against the actual value at
that position:
(require '[me.lomin.sinho.pred :refer [pred named-pred]])
;; Type check
(=* {:name (pred string?)} {:name "Alice" :age 30})
;; => {:name #Pred{...}, :age 30} (match)
;; Range check
(=* (pred #(< 0 % 100)) 50)
;; => #Pred{...} (match)
;; Named predicate for readable failure diffs
(=* (named-pred "shape?" #(isa? % ::shape)) :not-a-shape)
;; => #Mismatch{:expected #Pred{:label "shape?"}, :actual :not-a-shape}
Important: bare functions in expected positions are compared by value
equality, not interpreted as predicates. (=* string? string?) compares the
function objects. Use (pred string?) to match by predicate.
The A*-based matcher from sinho 2.2 has been replaced with a coinductive IR decision engine that walks the expected structure directly:
| Structure | sinho 2.2 | sinho 3.0 |
|---|---|---|
Map with n keys | O(n!) worst case | O(n) for atom keys; greedy bipartite fallback for complex keys |
Set of m elements in n-element actual | O(n!/(n-m)!) | O(n^3) (Kuhn's augmenting-paths for verdicts; greedy O(n^2 log n) for diffs) |
Sequence m vs n | O(3^(m+n)) | O(m*n) edit-distance DP |
Nested structure, depth d, width w | O((w!)^d) | O(w*d) amortised via seen-set |
| Atoms | O(1) | O(1) |
Every =* call that returned truthy in 2.2 returns truthy in 3.0, and every
falsy in 2.2 returns falsy in 3.0. The diff shape may differ in two cases:
Ambiguous sets of structurally-similar compound elements: The greedy
bipartite matching (strategy 2) may produce a non-minimum diff on adversarial
inputs. The diff is still correct (it accurately identifies structural
differences), but may not be the smallest possible diff. Verdicts use exact
bipartite matching (Kuhn's augmenting-paths, O(n^3)); only the diff
generation uses greedy (O(n^2 log n)).
Sequential elements with edit-distance alignment: The DP-based edit distance may choose a different alignment than the A* search when multiple alignments have the same cost.
Set matching breaks ties deterministically by (hash (str (pr-str l) (pr-str r))).
This ensures golden-master tests do not flake across JVM runs.
sinho 3.0 no longer depends on com.rpl/specter or mvxcvi/arrangement.
me.lomin.sinho.diff: tag-nav, children-nav, noop-terminal-nav,
terminal-nav, upsert-navigator, seq-nav, navs,
path-segment->navigator, node-diff-transformer, path->navigators,
leaf-diff-transformer and path-tree->diff-transformer. From
me.lomin.sinho.matcher: coll-walker+meta-nav.me.lomin.sinho.rank, sinho's own copy of
arrangement 2.1.0's public-domain comparator with a ClojureDart branch. On
the JVM it orders values exactly as arrangement does.lambdaisland/deep-diff2, which 2.2 reached only through Kaocha, is now a
direct dependency, so me.lomin.sinho.matcher/to-diff2 is defined on
Clojure, ClojureScript and Babashka. sinho 3.0 depends on
nubank/matcher-combinators, lambdaisland/deep-diff2 and
lambdaisland/kaocha.
The README's "Public API" names sinho's public API:
me.lomin.sinho.matcher/=*, me.lomin.sinho.matcher/to-diff2,
me.lomin.sinho.matcher-combinator/=*, me.lomin.sinho.pred/pred and
me.lomin.sinho.pred/named-pred. Every other var is internal.
=* matches and diffs the caller's values directly, instead of first
rebuilding every collection to add count metadata that nothing read, left
over from 2.2's A* search. Its diffs no longer carry
:me.lomin.sinho.matcher/count-seq metadata, and a collection the diff
does not rebuild keeps the caller's kind: a list, a lazy seq, a subvector
and a map entry keep theirs, an insertion at the front of a list conses
onto it, and on Babashka and ClojureScript an array map of more than eight
entries stays an array map until the diff replaces one of its keys.
Verdicts, costs and the caller's metadata are unchanged, and so are diffs,
except on ClojureScript where a set or map holds an empty sorted set: the
rebuild gave that set the hash 0, and the hash breaks ties between pairings
of equal cost, so such a diff may now pair other entries at the same cost.me.lomin.sinho.search and
me.lomin.sinho.a-star, and with the dependencies
org.clojure/data.priority-map and tailrecursion/cljs-priority-map that
only search used: from me.lomin.sinho.matcher, compute-cost,
compute-diff-paths, prepare, do-prepare, add-count-meta,
meta-count-xf, atom-count and atom-count-seq; from
me.lomin.sinho.diff, node-of?; from me.lomin.sinho.pred,
pred-label-for-diff.me.lomin.sinho.matcher, unexpected-type,
missing-type, mismatch-type, insertion?, deletion?, mismatch?,
equality-partition, make-ctx, assume-pair, pair-assumed?,
match-cost and coinductive-diff; from me.lomin.sinho.diff,
root-node, tag-ranking, comparable-vector, compare-paths,
grow-path-tree and none?; from me.lomin.sinho.matcher-combinator,
calculate-weight and sinho->result; from me.lomin.sinho.timeout,
now-us, Z95, ALPHA, SAFETY, DECAY, BURN-RATIO, KMAX,
SAMPLE-MAX-SPACING-MS, TARGET-OVERSHOOT-FRACTION and
TARGET-OVERSHOOT-CAP-MS.=* returns a diff into, or against, a sorted set or sorted map instead of
throwing "Cannot compare" on ClojureScript, a ClassCastException on the JVM
and Babashka, and a missing IComparable extension on ClojureDart.
An expected element or key that an actual sorted set or sorted map's
comparator cannot order is not in it, so (=* #{{2 :s}} (sorted-set 1))
returns #{#matcher_combinators.model.Mismatch{:expected {2 :s}, :actual 1}}.
An actual sorted collection whose comparator orders the expected value is
still searched with that comparator, so the diffs of every other match stay
the same.
When the diff replaces an element of a sorted set or a key of a sorted map
with a value its comparator cannot order, such as a Missing or Mismatch
record, that collection becomes a hash set or hash map with the same contents
and metadata. One whose comparator orders the replacement stays sorted, and a
diff that only replaces a sorted map's value keeps it sorted.
sinho compiles with ClojureDart, and =*, pred and the diff run on the Dart
VM with the same verdicts as on the JVM and diffs that are = to the JVM's.
A diff's collection types and the order of a large map can differ; the
README's "How a diff is built" lists them. On ClojureDart:
(is (=* expected actual)) passes whatever the diff, because ClojureDart's
is cannot be extended. Write (is (= expected (=* expected actual))).Mismatch prints as
{:expected 2, :actual 3}.to-diff2 and the match? adapter me.lomin.sinho.matcher-combinator
are unavailable, because neither deep-diff2 nor matcher-combinators' core
loads there.isa? hierarchies.bb test:bb runs every test namespace under test/cljc, found by name,
instead of a hand-kept list that had left out matcher-test and
pred-test.bb test:cljd runs the suite on the Dart VM.The README's "Running the tests" shows each dialect's command.
Previous release. See git history for details.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |