~ / track I / applied patterns
Matchers
IntermediatepatternA matcher is a function that says whether a data structure satisfies some condition — and is used to gate execution: run the next step only if the data matches. In languages like Haskell, F#, and Scala this pattern is front and center; in Clojure you build it yourself out of plain data and one interpreting function. This lesson picks up the rules-as-data abstraction from Indirection vs Abstraction and grows it into a matcher that composes.
Three different things get called "matchers" in Clojure-land. This lesson means validation rules expressed as data. It is not
core.match(which destructures by the shape of a value), and notmatcher-combinators(a test-assertion library). Same word, three ideas — keep them apart.
A boolean matcher
Start where the previous lesson left off: rules as data, interpreted by one function. Used to gate a "controller" that structures data and then acts on it.
cond-> threads the map through the assocclojure.core/assocReturn a new map with k -> v associated.view on clojuredocs → step only when match? returns
true; a failing match leaves the map without :status. This is the ideal shape
for a controller: structure the data, run it through a matcher, then execute
the effect. (Here the effect stand-in is assocclojure.core/assocReturn a new map with k -> v associated.view on clojuredocs →; in a real service that
gated step would be db/transact! or an HTTP call.)
A matcher that returns the value
A boolean is a dead end for composition — it tells you yes/no but doesn't carry
the data forward. The more useful matcher returns the value itself when it
matches, and nil when it doesn't. Merge the course and args into one
enrollment value so a matcher can read everything from a single map:
Now match composes with some->clojure.core/some->Threading that short-circuits on nil.view on clojuredocs →: the value flows into confirm on a match,
and a non-match short-circuits the whole pipeline to nil. This is the same
trick Clojure core uses everywhere — emptyclojure.core/emptyEmpty collection of the same type as coll.view on clojuredocs → returns the collection or nil,
and the comparators treat nil as falsey — so absence threads safely through a
pipeline. See nil, truthy values, and nil-punning.
One detail worth stealing: because a keyword is a function, :val can be
swapped for any function without changing match. For nested data, make it a
get-inclojure.core/get-inLook up a nested value through a path of keys.view on clojuredocs →: {:arg :party-size :val #(get-in % [:course :seats-left]) :comparator <=}.
This is not destructuring — that binds names by a value's shape. A matcher asserts a predicate over the value and returns it (or nothing) so the pipeline can continue or stop.
When the match fails
Returning nil loses why it failed. The next step is to return an error value
instead — a matcher that yields {:error ...} on a miss — which keeps failures
as data you can thread, log, and convert to an HTTP status. That's exactly the
move in Error handling: exceptions vs values,
and it composes the same way Railway-oriented programming
describes. The matcher is the gate; those lessons are about what flows down the
failure rail.
Check yourself
? quiz
Why does the second matcher return the value (or nil) instead of true/false?
Exercise
Implement match so the some->clojure.core/some->Threading that short-circuits on nil.view on clojuredocs → controller works: it must return the enrollment
map when every matcher holds, and nil otherwise. Edit match in the Repl
below; run until every assert passes.
Reference solution
(defn match [m matchers]
(when (every? (fn [{:keys [arg val comparator]}]
(comparator (arg m) (val m)))
matchers)
m))