~technomancy/fnl-match

Fennel's pattern matcher for Clojure
Don't attempt regex matches unless value is a string.
Improve documentation.

clone

read-only
https://git.sr.ht/~technomancy/fnl-match
read/write
git@git.sr.ht:~technomancy/fnl-match

You can also use your local clone with git send-email.

fnl-match

What if you could do pattern matching in Clojure? Wouldn't that be neat?

This is the pattern matcher from Fennel in Clojure.

Usage

Put this in your :dependencies in project.clj:

[fnl-match "0.1.0"]

The match macro evaluates its first argument, then searches thru the subsequent pattern/body clauses to find one where the pattern matches the value, and evaluates the corresponding body. Pattern matching can be thought of as a combination of destructuring and conditionals.

Example:

(match myvector
       59      :will-never-match-hopefully
       [9 q 5] (print :q q)
       [1 a b] (+ a b))

In the example above, we have a myvector value followed by three pattern/body clauses. The first clause will only match if myvector is 59. The second clause will match if myvector is a vector or list with 9 as its first element and 5 as its third element; if it matches, then it evaluates (print :q q) with q bound to the second element of myvector. The final clause will only match if myvector has 1 as its first element; if so then it will add up the second and third elements.

Patterns can be maps, vectors, regexes, literal values, or symbols. If a symbol has already been bound, then the value is checked against the existing local's value, but if it's a new local then the symbol is bound to the value. Regex patterns will be matched against values only if they are strings.

Vectors and maps can be nested. A vector that has fewer values in it than the vector pattern will have the remaining values bound to nil, same as when destructuring; likewise with map keys.

(match mymap
       {:v [a b c] :depth depth} (* b depth)
       _ :unknown)

Pattern matching performs unification, meaning that if x has an existing binding, clauses which attempt to bind it to a different value will not match:

(let [x 95]
 (match [52 85 95] 
   [b a a] :no ; because a=85 and a=95
   [x y z] :no ; because x=95 and x=52
   [a b x] :yes)) ; a and b are fresh values while x=95 and x=95

There is a special case for _; it is never bound and always acts as a wildcard. If no clause matches, it returns nil.

Sometimes you need to match on something more general than a structure or specific value. In these cases you can use guard clauses:

(match [91 12 53]
  ([a b c] ? (= 5 a)) :will-not-match
  ([a b c] ? (= 0 (mod (+ a b c) 2)) (= 91 a)) c) ; -> 53

In this case the pattern should be wrapped in parens, but the second thing in the parens is the ? symbol. Each form following this marker is a condition; all the conditions must evaluate to truthy for that pattern to match.

Differences from Fennel

  • No multi-valued match.
  • Support for regexes.
  • Nilability is opt-out (with guards) rather than opt-in.

License

Copyright © 2019 Phil Hagelberg

This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0.

This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version, with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html.