Liking cljdoc? Tell your friends :D

pattern

lasagna-pattern CI License: Unlicense

Part of the Lasagna Pattern toolbox.

Core pattern DSL for declarative matching and transformation of Clojure data structures. Pure data matching — no I/O, no storage, no HTTP.

Rationale

Extracting data from nested structures typically requires manual traversal code that's tedious to write and hard to maintain. This library provides a pattern language where you describe what you want, not how to get it:

  • Declarative — Patterns mirror the shape of your data
  • Variable binding — Extract values into named bindings
  • Constraints — Filter matches with predicates and defaults
  • Lazy compatible — Works with any ILookup implementation (databases, APIs, collections)
  • Schema validation — Optional compile-time validation with Malli
  • Cross-platform.cljc throughout, runs on CLJ and CLJS

Installation

;; deps.edn
{:deps {sg.flybot/lasagna-pattern {:mvn/version "0.1.2"}}}

;; Leiningen
[sg.flybot/lasagna-pattern "0.1.2"]

Only hard dependency is org.clojure/clojure. Optional deps: org.babashka/sci (sandboxed eval, required for CLJS), metosin/malli (schema validation).

Usage

Basic matching

(require '[sg.flybot.pullable :refer [match-fn rule apply-rules failure?]])

;; Create a matcher — returns bindings on match
(def m (match-fn '{:name ?n :age ?a} {:name ?n :age ?a}))
(m {:name "Alice" :age 30})
;=> {:name "Alice", :age 30}

;; Just extract a value
((match-fn '{:name ?n} ?n) {:name "Alice"})
;=> "Alice"

;; Use $ to access the original matched value
((match-fn '{:a ?x} (assoc $ :sum ?x)) {:a 1 :b 2})
;=> {:a 1 :b 2 :sum 1}

Constraints

;; :when — predicate constraint
(def adult (match-fn '{:age (?a :when #(>= % 18))} ?a))
(adult {:age 25})  ;=> 25
(adult {:age 10})  ;=> MatchFailure

;; :default — fallback value
((match-fn '{:name (?n :default "Anonymous")} ?n) {})
;=> "Anonymous"

Map value patterns

;; Literal values — exact equality check
((match-fn '{:type :user} $) {:type :user :name "Alice"})
;=> {:type :user :name "Alice"}

;; Functions — predicate check
((match-fn '{:age even?} $) {:age 30})   ;=> {:age 30}
((match-fn '{:age even?} $) {:age 31})   ;=> MatchFailure

;; Sets — membership check
((match-fn '{:status #{:active :pending}} $) {:status :active})
;=> {:status :active}

;; Regex — string pattern match
((match-fn '{:phone #"(\d{3})-(\d{4})"} $) {:phone "555-1234"})
;=> {:phone "555-1234"}

Sequence patterns

;; Fixed-length destructure
((match-fn '[?a ?b] [?a ?b]) [1 2])
;=> [1 2]

;; Head + rest (zero or more)
((match-fn '[?first ?rest*] {:first ?first :rest ?rest}) [1 2 3 4])
;=> {:first 1, :rest (2 3 4)}

;; One or more — fails on empty
((match-fn '[?items+] ?items) [1 2 3])  ;=> (1 2 3)
((match-fn '[?items+] ?items) [])       ;=> MatchFailure

;; Optional element — nil when absent
((match-fn '[?a ?b?] [?a ?b]) [1])    ;=> [1 nil]
((match-fn '[?a ?b?] [?a ?b]) [1 2])  ;=> [1 2]

;; Wildcard — skip without binding
((match-fn '[?_ ?second] ?second) [1 2])  ;=> 2

;; Unification — same var must match equal values
((match-fn '[?x ?x] ?x) [1 1])  ;=> 1
((match-fn '[?x ?x] ?x) [1 2])  ;=> MatchFailure

Transformation rules

;; Pattern → template transformation
(def double-to-add (rule '(* 2 ?x) '(+ ?x ?x)))
(double-to-add '(* 2 5))  ;=> (+ 5 5)
(double-to-add '(* 3 5))  ;=> nil (no match)

;; Apply rules recursively (bottom-up)
(def simplify-add-0 (rule '(+ 0 ?x) ?x))
(apply-rules [double-to-add simplify-add-0] '(+ 0 (* 2 y)))
;=> (+ y y)

Failure handling

(let [result ((match-fn '{:x ?x} ?x) "not a map")]
  (when (failure? result)
    {:reason (:reason result)
     :path   (:path result)
     :value  (:value result)}))
;=> {:reason "expected map", :path [], :value "not a map"}

Pattern Syntax

Variables

PatternDescription
?xBind value to x
?_Wildcard (match anything, no binding)
?x?Optional (0-1 elements, sequences only)
?x*Zero or more (lazy)
?x+One or more (lazy)
?x*!Zero or more (greedy)
?x+!One or more (greedy)

Extended variable options

PatternDescription
(?x :when pred)Constrained match
(?x :default val)Default on failure
(?x :when pred :default val)Both chained (default first, then predicate)
(?x* :take N)Take up to N elements. Pair with ?_* for rest.
(?_ :skip N)Skip exactly N elements

Structural patterns

PatternDescription
{}Map pattern (maps + ILookup)
[]Sequence pattern (any seqable)
{{:id 1} ?result}Indexed lookup (ILookup)
$Original input (in match-fn body)

Map value shortcuts

Value in patternBehavior
?xVariable binding
:keyword / 42 / "str"Exact equality check
even? (function)Predicate — (even? val)
#{:a :b} (set)Membership — (contains? #{:a :b} val)
#"regex"Regex match on string values

Map Matching

Map patterns work with both standard Clojure maps and any ILookup implementation:

;; Standard map — preserves unmatched keys
((match-fn '{:a ?x} $) {:a 1 :b 2 :c 3})
;=> {:a 1 :b 2 :c 3}  ; :b and :c preserved

;; ILookup — only returns matched keys (can't enumerate)
;; Used for lazy data sources (databases, collections)

;; Indexed lookup with non-keyword keys
{{:id 1} ?user}      ; lookup by map query
{[0 1] ?cell}        ; lookup by vector key

Schema Validation

Optional compile-time validation using :schema option:

;; With Malli schemas (require sg.flybot.pullable.malli first)
(require '[sg.flybot.pullable.malli])
(require '[malli.core :as m])

((match-fn '{:name ?n} ?n {:schema (m/schema [:map [:name :string]])})
 {:name "alice"})
;=> "alice"

;; Indexed lookup requires :ilookup annotation
(def api-schema
  (m/schema [:map [:users [:vector {:ilookup true}
                           [:map [:id :int] [:name :string]]]]]))

;; Now both patterns are valid:
'{:users ?all}              ; LIST — always valid
'{:users {{:id 1} ?user}}   ; GET — requires :ilookup true

Schemas also act as visibility control — only declared keys are accessible to patterns. Undeclared keys become effectively private.

API Reference

Core

FunctionKindSignatureDescription
match-fnMacro[pattern body] or [pattern body opts]Create matcher function with variable bindings
ruleMacro[pattern template]Create pattern → template transformation
apply-rulesFn[rules data]Apply rules recursively (bottom-up postwalk)
failure?Fn[x]Predicate for MatchFailure records

Options (match-fn / compile-pattern)

OptionDescription
:schemaMalli or built-in schema for compile-time validation
:rulesCustom rewrite rules (prepended to defaults)
:onlyReplace all default rules with these
:resolveCustom symbol resolver (fn [sym])
:eval-fnCustom form evaluator (fn [form])

Extension

FunctionSignatureDescription
register-var-option![key handler-fn]Register custom variable option (like :when)
register-schema-rule![rule-fn]Register custom schema type inference
get-schema-info[schema]Query schema type and structure

Development

bb test pattern    # Run full test suite (Kaocha + RCT)
bb rct pattern     # Run RCT tests only
bb dev pattern     # Start REPL

See CLAUDE.md for architecture, internals, and extension points. See doc/performance-analysis.md for complexity characteristics.

Can you improve this documentation? These fine people already did:
loicb, Luo Tian & Robert Luo
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close