meander

repository·epsilon·Indexed 21 days ago

https://github.com/noprompt/meander

A Clojure/ClojureScript library for transparent data transformation using pattern matching and logic variables. It provides macros like m/match, m/search, and m/find for extracting and joining data, as well as a strategy system (meander.strategy.epsilon) for multi-step, in-place transformations including top-down and bottom-up rewrites. Key features include memory variables for value collection, m/cata for recursive processing, and m/defsyntax for extending pattern syntax.

Tokens
14.1K
Snippets
58
Records
61
Agent score
75%

What's inside meander

  1. Perform recursion and aggregation with m/cata

    epsilon

    The m/cata operator allows patterns to call themselves, enabling recursive processing and accumulation of results. This is useful for reducing a nested structure into a single value or transforming nested maps/sequences.

    ;; Example: Reducing a list to a single value via recursion
    (m/rewrite [() '(1 2 3)]
      [?current (?head & ?tail)]
      (m/cata [(?head & ?current) ?tail])
    
      [?current ()]
      ?current)
    ;; => (3 2 1)
  2. How Meander's pattern matching macros work

    epsilon

    Meander uses a macro-based system to match an input against a series of clauses. Each clause consists of a pattern (LHS) and an expression (RHS).

    • Input: The data item being matched (can be a literal or a variable).
    • Pattern (LHS): Composed of literal data and Meander's matching operators. Patterns must be literals at compile time.
    • Expression (RHS): The code evaluated if the pattern matches.
      • In match, search, and find, the RHS is arbitrary Clojure code.
      • In rewrites and rewrite, the RHS uses Meander's specific substitution operations.

    The general syntax is:

    (m/matching-macro input
        pattern1 expression1
        pattern2 expression2
        ...)
    (m/match input
      pattern1 expression1
      pattern2 expression2)
  3. How strategies and strategy combinators work

    epsilon

    In Meander, a strategy is a function that takes a term t and returns a rewritten term t*.

    A strategy combinator is a higher-order function that accepts one or more strategies and returns a new strategy. Combinators allow you to compose simple transformations into complex rewriting logic.

    All combinators are located in the meander.strategy.epsilon namespace.

    Failure Handling: When a strategy or combinator fails to transform a term, it returns a special failure value: meander.strategy.epsilon/*fail* (printed as #meander.epsilon/fail[]). You can check for failure using meander.strategy.epsilon/fail?.

    (require '[meander.strategy.epsilon :as r])
    
    ;; A strategy is a function: (strategy term) -> rewritten-term
    ;; A combinator returns a strategy: (combinator strategy1 strategy2) -> strategy
  4. Use memory variables to accumulate values

    epsilon

    Memory variables are prefixed with an exclamation point (e.g., !xs) and accumulate matched values into a vector. They can be used with match, search, find, rewrites, and rewrite. When combined with the zero-or-more repetition operator ..., they collect all elements matched by that pattern into the memory variable.

    Example of accumulating elements into a vector using match:

    (m/match [1 2 3]
      [!xs ...]
      !xs)
    ;; => [1 2 3]
  5. Understand Substitution in Meander

    epsilon

    Substitution is the inverse of pattern matching. While pattern matching deconstructs objects to bind values, substitution uses existing bindings to construct new objects. It is a core component of Meander's rewriting macros and is used for data transformation.

    To use substitution, you must require the meander.epsilon namespace.

    (require '[meander.epsilon :as m])
  6. Transform sequences using m/rewrite and m/cata

    epsilon

    For complex sequence transformations (like parsing a custom string format into a structured map), m/rewrite combined with m/cata is the recommended approach.

    m/cata (catamorphism) allows you to recursively apply a transformation to elements within a sequence. You can use it on the 'left side' (the input) or the 'right side' (the generation/output) of a rewrite rule to process sub-elements.

    • Left-side cata: Processes the input elements before they are matched.
    • Right-side cata: Processes the generated elements to construct a final value.
    • Dual-side cata: Using a placeholder like $EXAMPLE to define how sub-elements should be recursively rewritten.
    ;; Final Solution using cata on the right side to construct values recursively
    (m/rewrite ["oppas" "obj1" "@attr1" "@attr2" "obj2"]
      [(m/re "#obj|oppas|dc" ?ns) . !segs ...]
      {:ns (m/keyword ?ns)
       :xsegs [(m/cata ($EXAMPLE !segs)) ...]}
    
      ($EXAMPLE (m/re "#@.*" ?val))
      {:kind :seg-attr :val ?val}
    
      ($EXAMPLE (m/re "#[^@].*" ?val))
      {:kind :seg-chld :val ?val}
    
      ($EXAMPLE ?val)
      {:kind :unknown :val ?val})
  7. Handle both matching and substitution contexts in `defsyntax`

    epsilon

    When defining a defsyntax operator, you must account for the fact that it might be used in a matching context (the left side of m/match, m/search, m/find, m/rewrite, or m/rewrites) or a substitution context (the right side of m/rewrite/m/rewrites, or within m/subst).

    If your operator expands into a complex pattern (like m/pred or m/app), it may fail during substitution because m/app attempts to apply a function to values that aren't ready yet, leading to errors like java.lang.ClassCastException.

    To make an operator safe for both contexts, use m/match-syntax? and m/subst-syntax? to inspect &env. For substitution contexts, it is common to return the original &form unchanged.

    (m/defsyntax ident [ns-pattern name-pattern]
      (if (m/match-syntax? &env)
        ;; Return a complex pattern for matching
        `(m/pred ident? (m/app namespace ~ns-pattern) (m/app name ~name-pattern))
        ;; Return the original form for substitution
        &form))
  8. Understand Literal Patterns

    epsilon

    Literal patterns match exactly. They include:

    • Scalar data types (numbers, strings, booleans, keywords).
    • Quoted or unquoted symbols that are not special Meander variables.
    • Lists and vectors composed of literals (that do not contain maps, sets, or subsequence operators).

    Note on Maps and Sets: Unlike lists/vectors, map and set patterns express submap and subset patterns. A map pattern {:foo 1} matches any map containing at least that key-value pair, even if it has additional keys.

    ;; A literal list pattern
    (m/match [1 2] [1 2] :ok)
    
    ;; A submap pattern (matches even if extra keys exist)
    (m/match {:foo 1 :bar 2} {:foo 1} :ok)
  9. Use `m/scan` for collection traversal and joins

    epsilon

    m/scan is a specialized pattern for traversing collections. It provides a simpler syntax for the complex wildcard matching pattern [_ ... pattern ... . _ ...].

    Capabilities:

    • Traversal: It allows you to iterate through elements of a collection and bind them to logic variables.
    • Internal Joins: You can use the same logic variable in multiple m/scan calls within a single m/search expression to perform operations similar to SQL or Datalog joins. The search will only succeed if the variable matches across all scans.
    • Complexity Note: m/search constructs matching code based on the order of keys in the input map. Because Clojure maps (specifically PersistentHashMap) do not guarantee key order, the order of results from m/search may vary depending on the size of the map and its internal storage implementation.
    ;; Simple scan: finding values following a 1 in a vector
    (m/search [1 2 1 3 1 5]
      (m/scan 1 ?x) ?x)
    ;; => (2 3 5)
    
    ;; Scanning multiple collections to perform a join
    (m/search {:people [{:id 1 :name "Bob"} {:id 2 :name "Alice"}]
               :addresses [{:type :business :person-id 1 :info ""}
                           {:type :other :person-id 1 :info ""}
                           {:type :business :person-id 2 :info ""}
                           {:type :vacation :person-id 2 :info ""}]}
    
      {:people (m/scan {:name ?name :id ?id})
       :addresses (m/scan {:person-id ?id :as ?address})}
    
      {:name ?name :address ?address})
    ;; => ({:name "Bob", :address {:type :business, :person-id 1, :info ""}}
    ;;     {:name "Bob", :address {:type :other, :person-id 1, :info ""}}
    ;;     {:name "Alice", :address {:type :business, :person-id 2, :info ""}}
    ;;     {:name "Alice", :address {:type :vacation, :person-id 2, :info ""}})
  10. Capture repeating elements with logic and memory variables

    epsilon

    When using subsequence operators, you can capture the repetition using different variable types:

    • Logic Variables (!var): Used to capture the count or the sequence itself within the pattern logic.
    • Memory Variables (!var): Used to capture multiple repeats. This is particularly useful for handling nested groups where standard matching might behave unexpectedly. To ensure correct behavior with nested groups, capture the number of times things repeat using ..!n.
    ;; logic variable capture
    (m/match [:a :b :c] [!xs ..?n] [!xs ?n]) ;; => [[:a :b :c] 3]
    
    ;; memory variable capture for nested groups
    (m/rewrite [:a [1 2 3] :b [4 5]] [!k [!x ..!n] ..!m] [!k [!x ..!n] ..!m])
    ;; => [:a [1 2 3] :b [4 5]]
  11. Use Memory Variables in substitution

    epsilon

    Memory variables (prefixed with !) disperse their values throughout a substitution. Each occurrence of the variable consumes one element from the collection it is bound to.

    Key behaviors:

    • Dispersal: Values are spread across the pattern until the collection is exhausted.
    • Subsequence patterns: In patterns like ... or ..n, values are dispersed until a memory variable is exhausted.
    • Exhaustion: If the pattern requires more elements than the memory variable's collection contains, nil is dispersed for the remaining occurrences.
    • Repetition: nil is also dispersed in n or more patterns up to n if the collections are exhausted.
    ;; Basic dispersal
    (let [!xs [1 2 3]]
      (m/subst (!xs !xs !xs)))
    ;; => (1 2 3)
    
    ;; Dispersal with subsequence patterns
    (let [!bs ['x 'y]
          !vs [1 2 3]]
      (m/subst [!bs !vs ...]))
    ;; => [x 1 y 2]
    
    ;; Exhaustion results in nil
    (let [!xs [1]]
      (m/subst (!xs !xs !xs)))
    ;; => (1 nil nil)
    
    ;; nil dispersal in repetition patterns
    (let [!xs ['A]
          !ys [:B]]
      (m/subst (!xs !ys ..2)))
    ;; => (A :B nil nil)
  12. Transform data using `rewrite` and `rewrites` with substitutions

    epsilon

    While match, search, and find use arbitrary Clojure code in their Right-Hand Side (RHS) expressions, rewrite and rewrites use Meander substitutions. A substitution is the inverse of a pattern: instead of deconstructing data, it constructs data by filling in pieces using Meander variables.

    • rewrite: Returns the first match using a substitution in the RHS.
    • rewrites: Returns potentially multiple matches using substitutions in the RHS.

    This declarative approach is often more concise than using arbitrary Clojure code.