~ / track I / applied patterns

Matchers

Intermediatepattern

A 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 not matcher-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.

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

cond-> threads the map through the a‌s‌s‌o‌c 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 a‌s‌s‌o‌c; 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:

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

Now match composes with s‌o‌m‌e‌-‌>: 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 — e‌m‌p‌t‌y 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 g‌e‌t‌-‌i‌n: {: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 s‌o‌m‌e‌-‌> 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.

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate
Reference solution
(defn match [m matchers]
  (when (every? (fn [{:keys [arg val comparator]}]
                  (comparator (arg m) (val m)))
                matchers)
    m))
 status: new