Recursive, data driven, pattern matching for Clojure
[motif "1.0.1"]
motif/motif {:mvn/version "1.0.1"}
<dependency>
<groupId>motif</groupId>
<artifactId>motif</artifactId>
<version>1.0.1</version>
</dependency>
Motif brings expressive pattern matching to Clojure. Motif focuses on using the power of our core Clojure data structures, giving each its own meaning and possibilities.
Let's get started!
(require '[motif.core :refer [matches?] :as motif])Simple value patterns are simply compared via equality.
(matches? 1 1) ;=> true
(matches? 1 2) ;=> falseFunctions as patterns are invoked on the given target. Monadic predicates are only supported.
(matches? pos? 1) ;=> true
(matches? string? 1) ;=> false
(matches? inc 1) ;=> true
(matches? (fn [x y]) 1) ;=> false, due to ArityExceptionRegex patterns are compared to the string representations of their targets as with clojure.core/re-matches.
(matches? #"\d*" "123") ;=> true
(matches? #"\d*" 123) ;=> true
(matches? #"\[(\d*\s)*\d*\]" [1 2 3]) ;=> trueVectors compare each element ordinally, that is the first element of the pattern is compared to the first of the target, the second to the second, and so on. Vectors require their targets to be at least as long as their patterns, but the targets can be longer than the patterns.
(matches? [1 2] [1 2]) ;=> true
(matches? [1 2] [1]) ;=> false
(matches? [1 2] [1 2 3]) ;=> true
(matches? [pos? neg?] [1 -1]) ;=> true
(matches? [\a \b] "ab") ;=> trueSeq patterns work similarly to vector patterns, however, they can be longer or shorter than their targets. This allows for infinite length sequences to be used as patterns, however, beware of infinite loops!
(matches? '(1 2 3) [1]) ;=> true
(matches? '(1) [1 2 3 4]) ;=> true
(matches? (repeat even?) [2 2 2 2]) ;=> true
(matches? (repeat odd?) (repeat 1)) ;=> infinite loop!Maps compare corresponding key values with matches? Maps a conjunctive, and thus all keys much match positively. Extra keys in the target map are acceptable, and ignored by the pattern. Keys in the pattern are not required to be in the target map.
(matches? {:key :pattern-value} {:key :target-value}) ;== (matches? :pattern-value :target-value)
(matches? {1 2} {1 2 3 4}) ;=> true
(matches? {1 2 3 4} {1 2}) ;=> false
(matches? {:key1 :value :key2 nil} {:key1 :value}) ;=> true
(matches? {:key1 :value :key2 nil} {:key1 :value :key2 nil}) ;=> trueIf a key in the pattern is an function (ifn?), it is instead applied to the target as apposed to gotten with get; this opens up another dimension of expressiveness using maps.
(matches? {:key 1 #(count (keys %)) 1} {:key 1}) ;=> true
(matches? {pos? false neg? false} 0) ;=> trueSets are our disjunctive patterns; they need only one of their elements match against the target.
(matches? #{1 2 3} 1) ;=> true
(matches? #{even? odd?} 0) ;=> true
(matches? #{pos? neg?} 0) ;=> falseModifiers extend, specify, and hack how the patterns work. Modifiers are attached to their patterns as metadata values.
The Equality modifier directs motif to compare values using the = function, rather than matches?. We can use this when motif's special interpretations might get in the way.
(matches? {identity 1} {identity 1}) ;=> false
(matches? ^:= {identity 1} {identity 1}) ;=> trueThe Use modifier explicitly directs motif to use a different function than matches?. The Equality modifier is a special case of the use modifier.
(defn same-keys?
[m0 m1]
(= (set (keys m0))
(set (keys m1))))
(matches? {:x 1 :y 2} {:x 3 :y 4}) ;=> false
(matches? ^{:use same-keys?} {:x 1 :y 2} {:x 3 :y 4}) ;=> trueExclusive to maps, this modifier explicitly defines how keys are used to access their values in maps. In cases where you have a map with function keys, this may be useful to stop them from being applied to the map and instead be gotten with get.
(matches? ^{:getter get} {pos? neg? neg? pos?} {pos? -2 neg? 2}) ;=> trueThe Star modifier maps the pattern over the target, requiring all elements to match. More specifically, the target is seq'd, and each element is matched against the pattern. If every element matches, the pattern matches.
(matches? #{1 2} [1 2]) ;=> false
(matches? ^:* #{1 2} [1 2]) ;=> trueAdditionally, a positive integral value can be passed to the star modifier. The number will define how many extra times motif should seq targets before matching against the pattern. The default value is 0, and thus only does one seq.
(matches? ^{:* 1} (fn [n] (integer? n)) [[1 2] [3 4] [5 6]]) ;=> trueSince modifiers take up the information space of the structures metadata, the Meta Modifier adds another area for metadata to be matched. That is, the metadata of the target is matched against the values in the Meta Modifier. The values are matched in the same way as in matches?, so all patterns discussed work within the modifier.
(matches? {:y 1} ^{:x 1} {:y 1}) ;=> true
(matches? ^{:meta {:x 2}} {:y 1} ^{:x 1} {:y 1}) ;=> false
(matches? ^{:meta {:x ^:* #{1 2}}} {:y 1} ^{:x [1 2]} {:y 1}) ;=> trueThe Strict modifier is interpreted different for each pattern type, though, it in general reduces some of the laxness that our patterns might have.
Strict maps require equality in keys.
(matches? {:x 1} {:x 1 :y 2}) ;=> true
(matches? ^:! {:x 1} {:x 1 :y 2}) ;=> false
(matches? ^:! {:x 1 :y 2} {:x 1 :y 2}) ;=> trueSets require at least one element to match the target. Strict sets strengthen this requirement to require one and only one element to match the target.
(matches? #{1 pos?} 1) ;=> true
(matches? ^:! #{1 pos?} 1) ;=> falseStrict Vectors require the length of their targets be the same as their own length.
(matches? [1 2 3] [1 2 3 4]) ;=> true
(matches? ^:! [1 2 3] [1 2 3 4]) ;=> falseIt is worth noting the implicit ability to create logical patterns using our given tools. Maps require all pattern elements to match, a natural implementation of and. Sets require only one pattern to match, being an implementation of or. Finally, strict sets require one and only one element match the pattern, a perfect representation of xor. Here's a few examples:
; Or
(matches? #{integer? pos? odd?} -2) ;=> true
; And
(matches? {integer? true pos? true even? true} 2) ;=> true
; Xor
(matches? ^:! #{pos? neg?} 1) ;=> true
(matches? ^:! #{pos? neg?} 0) ;=> false
(matches? ^:1 #{pos? odd?} 1) ;=> falseHere we find ourselves with a slight discomfort: It can increase tedium and decrease legibility to write true as a value for simple and maps. Similarly, or as sets has less robustness since only boolean values can be compared. To fix this, we have additional modifiers ^:& and ^:| to imply conjunctive/disjunctive natures, respectively.
; or map
(matches? ^:| {:x 1 :y 2} {:x 1 :y 3}) ;=> true
; and set
(matches? ^:& #{pos? even?} -2) ;=> falsematches? is not symmetric! That is (matches? a b) does not imply (matches? b a). This is due to patterns being treated differently that their targets. Consider the following:
(matches {:x 1 nil? true} {:x 1}) ;=> false
(matches {:x 1} {:x 1 nil? true}) ;=> trueThis asymmetry is due to how various structures are interpreted when they are used as patterns.
Any exceptions thrown by patterns are treated as general failures, causing motif to return false.
(matches? #(throw (Exception. "Some Exception")) nil) ;=> falseHowever, be aware that exceptions will be thrown during the pattern compilation step; so if there's something wrong with your pattern, it will be represented as an exception.
In most instances, clojure implements sets and maps using hashing, which does not guarantee ordering. There may arise some cases where you want one element of a pattern to be executed before another. In these cases one should remember the use of array-map and, though not included in core, ordered-set.
As some modifiers completely change the course of interpretation, there is an implicit precedence in which some tags nullify others.
The ^:use and ^:= tags void all other tags, as the matches? semantics are disregarded.
Strictness in sets ^:! supersedes conjunction ^:&.
Strictness in maps ^:! does not interfere with disjunction ^:|, though will result in strange effects. It is likely these effects are not desired, so avoid using both.
Meta ^:meta and star ^:* tags play nicely with others.
Due to how the compiler has been implemented, meta macros are not picked up when applied to symbols referring to vars ((meta ^:x meta) ;=> nil). When modifiers are needed on variable values, remember the with-meta function ((meta (with-meta meta {:x true})) ;=> {:x true}. This also applies to function application results ((with-meta (identity inc) {:x 1})).
Function literals, however, pick up meta macros just fine. Consider wrapping your function in a literal: (matches? ^:* (fn [x] (integer? x)) [[1 2 3] [4 5 6] [7 8 9]]).
We've added a convenience function to the library. _ is the same as clojure.core/any?, but has a more idiomatic feel to it.
(require '[motif.core :refer [_ matches?]])
(matches? _ 1) ;=> true
(matches? _ nil) ;=> true
(matches? {:x _} {:x {:y 1}}) ;=> trueIncluded in motif.core is thematch macro. match works the same as (condp = x ...).
(match 1
neg? "negative"
zero? "zero"
pos? "positive") ;=> positiveThat's all you need to go out into the world. But before you go, let's look at some fun examples that might help illuminate some of the possibilities:
(matches? (interleave (repeat odd?) (repeat even?)) [1 2 3 4]) ;=> true
(matches? {first 1 last 4} [1 2 3 4]) ;=> true
(matches? {0 1 1 2 2 3 3 4} [1 2 3 4]) ;=> true
(matches? {some? true inc 1} 0) ;=> true
(matches? {(partial reduce max) 4} [1 2 3 4]) ;=> true
(matches? (complement #{1 2 3}) 4) ;=> true
(matches? {:x nil keys ^:* #{:x}} {:x nil}) ;=> true
(matches? {:x nil keys ^:* #{:x}} {:x nil :y nil}) ;=> false
(matches? ^:* ^:! {:x pos?} [{:x 1} {:x 2} {:x 3} {:x 4}]) ;=> true
(matches? ^{:meta {(comp set keys) ^:= #{:x :y}}} {:a pos?} ^{:x 1 :y 2} {:a 1}) ;=> true
(matches? ^{:use clojure.set/subset?} #{1 2 3} #{1 2 3 4}) ;=> true
(matches? ^{:getter (fn [target key] (inc key))} {0 1 1 2 2 3} {})