Extending match for new Patterns

glchapman edited this page Mar 26, 2012 · 4 revisions

The pattern matcher is easily extensible by customizing several multimethods.

Regular Expression syntax

Conversion to Pattern matrix

First, we need to add a new method for emit-pattern.

emit-pattern is used in the initial traversal of the pattern matrix, converting normal Clojure forms into patterns.

emit-pattern dispatches on the class of a form. When the conversion process comes across a regex pattern, we'd like to drop in our customized pattern.

What to dispatch on?

match.regex=> (type #"asdf")

So we add a simple dispatch that returns a RegexPattern record.

(defrecord RegexPattern [regex])

(defmethod emit-pattern java.util.regex.Pattern
    (RegexPattern. pat))

Defining equality

We use the multimethod pattern-compare to compare patterns in the compilation phase.

The default method of pattern-compare says that the two arguments are not equal.

For our extension this is fine, except for the case where two RegexPatterns have equal regular expressions.

Let's define that case.

(defmethod pattern-compare [RegexPattern RegexPattern]
  [a b] (if (and (= (.pattern (:regex a)) (.pattern (:regex b)))
                 (= (.flags (:regex a)) (.flags (:regex b))))

Converting Pattern to Source

If we attempt to pattern match currently, the regex syntax will be treated as a normal LiteralPattern, which simply uses = to test a branch.

match.core.debug=> (m-to-clj [x]
                             [#"a"] 1
                             :else 2)
  (= x #"a") 1 
  :else 2)

What we really want is this:

 (re-matches #"a" x) 1
 :else 2)

To accomplish this we customize the multimethod to-source

(defmethod to-source RegexPattern
  [pat ocr]
  `(re-matches ~(:regex pat) ~ocr))

to-source dispatches on the type of its first argument. This method will be chosen when the pattern is a RegexPattern.

The second argument is the "occurance" we're currently matching. In the example above, the occurance is "x".

We can run to-source directly to see what we get.

match.regex=> (def r (RegexPattern. #"a"))
;=> #'match.regex/r
match.regex=> (to-source r 'x)
;=> (clojure.core/re-matches #"a" x)

Using the extension

We simply need to use the namespace containing the customizations to include them in pattern matching.

match.core.debug=> (use 'match.regex)
match.core.debug=> (m-to-clj [x]
                             [#"a"] 1
                             :else 2)
  (re-matches #"a" x) 1 
  :else 2)