Andrey Listopadov: 1 implement new prefix syntax for match macro 3 files changed, 148 insertions(+), 22 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~technomancy/fennel/patches/20430/mbox | git am -3Learn more about email & git
New syntax replaces and extend old question mark `?' based guards with prefix `where' guards and optional pattern ORing with `or' special form. Current implementation uses transformation from new syntax into old match syntax and passes the result to old implementation of match* macro. I've found this way much easier to integrate and also maintain new syntax (like if we will decide to allow `or' outside of where we will need to add one additional line to current implementation). Incorporating new syntax into old macro implementation proven to result in quite ugly code, that IMO will be hard to understand and maintain in the future. I've added tests for the new syntax, and ensured that all old tests pass. I've also removed old syntax from the reference.md. It is still accessible and will continue to work, but I think we should encourage to use prefix notation by default.
It might be a good idea to keep a brief mention of the old style in just in case people come across some old code and want to know why it works, similar to how require-macros is mentioned but is advised against. (In fact, the parallels to require-macros are strong because import-macros also expands to code which uses the old deprecated form.)
Below some examples of new syntax and its old syntax variants. Guard clauses: ;; old (match [1 2 3] ([a b c] ? (= a 1) (> b 0) (= (// c 3) 0)) :match :no-match) ;; new (match [1 2 3] (where [a b c] (= a 1) (> b 0) (= (// c 3) 0)) :match :no-match) Sharing same body across patterns: ;; old (match [1 2 3] [4 5 6] :match [1 2 3] :match :no-match) ;; new (match [1 2 3] (where (or [4 5 6] [1 2 3])) :match :no-match) Combined guards and pattern oring: ;; old (match [1 2 3] ([a 2 5] ? (> a 0)) :match ([a 2 3] ? (> a 0)) :match :no-match) ;; new (match [1 2 3] (where (or [a 2 5] [a 2 3]) (> a 0)) :match :no-match) --- reference.md | 27 ++++++++-------- src/fennel/macros.fnl | 70 +++++++++++++++++++++++++++++++++++++++-- test/macro.fnl | 73 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 148 insertions(+), 22 deletions(-) diff --git a/reference.md b/reference.md index ac96573..7eed5d0 100644 --- a/reference.md +++ b/reference.md @@ -107,10 +107,10 @@ This style of anonymous function is useful as a parameter to higher order functions, such as those provided by Lua libraries like lume and luafun. -The current implementation only allows for either functions functions with -up to 9 arguments, each named `$1` through `$9`, or those with varargs, -delineated by `$...` instead of the usual `...`. A lone `$` in a hash function -is treated as an alias for `$1`. +The current implementation only allows for hash functions to use up to +9 arguments, each named `$1` through `$9`, or those with varargs, +delineated by `$...` instead of the usual `...`. A lone `$` in a hash +function is treated as an alias for `$1`. Hash functions are defined with the `hashfn` macro or special character `#`, which wraps its single argument in a function literal. For example, @@ -393,18 +393,19 @@ or specific value. In these cases you can use guard clauses: ```fennel (match [91 12 53] - ([a b c] ? (= 5 a)) :will-not-match - ([a b c] ? (= 0 (math.fmod (+ a b c) 2)) (= 91 a)) c) ; -> 53 + (where [a b c] (= 5 a)) :will-not-match + (where [a b c] (= 0 (math.fmod (+ a b c) 2)) (= 91 a)) c) ; -> 53 ``` -In this case the pattern should be wrapped in parens (like when -matching against multiple values) but the second thing in the parens -is the `?` symbol. Each form following this marker is a condition; -all the conditions must evaluate to true for that pattern to match. +In this case the pattern should be wrapped in parentheses (like when +matching against multiple values) but the first thing in the +parentheses is the `where` symbol. Each form following this marker is +a condition; all the conditions must evaluate to true for that pattern +to match. -**Note:**: The `match` macro can be used in place of the `if-let` macro -from Clojure. The reason Fennel doesn't have `if-let` is that `match` -makes it redundant. +**Note:**: The `match` macro can be used in place of the `if-some` +macro from Clojure. The reason Fennel doesn't have `if-some` is that +`match` makes it redundant.
We talked about this on IRC but I think we should keep this as-is because if-let is a frequently asked question and no one has yet asked about if-some, even though if-some has a behavior closer to match.
### `global` set global variable diff --git a/src/fennel/macros.fnl b/src/fennel/macros.fnl index 911a80d..ea3581b 100644 --- a/src/fennel/macros.fnl +++ b/src/fennel/macros.fnl @@ -347,7 +347,6 @@ introduce for the duration of the body if it does match." syms)) (fn match* [val ...] - "Perform pattern matching on val. See reference for details." (let [clauses [...] vals (match-val-syms clauses)] ;; protect against multiple evaluation of the value, bind against as @@ -355,10 +354,77 @@ introduce for the duration of the body if it does match." (list `let [vals val] (match-condition vals clauses)))) +;; Construction of old match syntax from new syntax
I prefer this implementation to the other one as well. One point of clarification though; it seems that the compiler output of this macro will double-up (or triple-up, etc) the body associated with any pattern that uses `or`. Is this true of the other implementation as well? Also I think it would be good to be a little more explicit in the comments that the `match*` function strictly implements a version of `match` that does not support `where` clauses and that the implementation of `where` is handled below.
+ +(fn partition-2 [seq] + ;; Partition `seq` by 2. + ;; If `seq` has odd amount of elements, the last one is dropped. + ;; + ;; Input: [1 2 3 4 5] + ;; Output: [[1 2] [3 4]] + (let [firsts [] + seconds [] + res []] + (for [i 1 seq.n 2] + (table.insert firsts (or (. seq i) 'nil)) + (table.insert seconds (or (. seq (+ i 1)) 'nil))) + (each [i v1 (ipairs firsts)] + (let [v2 (. seconds i)] + (if (not= nil v2) + (table.insert res [v1 v2])))) + res)) + +(fn transform-or [[_ & pats] guards] + ;; Transforms `(or pat pats*)` lists into match `guard` patterns. + ;; + ;; (or pat1 pat2), guard => [(pat1 ? guard) (pat2 ? guard)] + (let [res []] + (each [_ pat (ipairs pats)] + (table.insert res (list pat '? (unpack guards)))) + res)) + +(fn transform-cond [cond] + ;; Transforms `where` cond into sequence of `match` guards. + ;; + ;; pat => [pat] + ;; (where pat guard) => [(pat ? guard)] + ;; (where (or pat1 pat2) guard) => [(pat1 ? guard) (pat2 ? guard)] + (if (and (list? cond) (= (tostring (. cond 1)) :where)) + (let [second (. cond 2)] + (if (and (list? second) + (= (tostring (. second 1)) :or))
This should be able to use = on a symbol now that we have the __eq metamethod.
+ (transform-or second [(table.unpack cond 3)]) + :else + [(list second '? (table.unpack cond 3))])) + :else + [cond])) + +(fn match** [val ...]
Let's internally name this function `match-where` to make it clearer that the `where` support is why it's a separate function.
+ "Perform pattern matching on val. See reference for details. + +Syntax: + +(match data-expression + pattern body + (where pattern guard guards*) body + (where (or pattern patterns*) guard guards*) body + else-body)"
Let's avoid mentioning the `else-body` argument. It's supported but I think it's better if we don't mention it and encourage people to match against `_` instead. Thanks for working on this! I'm looking forward to using the new functionality in my pattern matches. =) -Phil
+ (let [conds-bodies (partition-2 (table.pack ...)) + else-branch (if (not= 0 (% (select :# ...) 2)) + (select (select :# ...) ...)) + match-body []] + (each [_ [cond body] (ipairs conds-bodies)] + (each [_ cond (ipairs (transform-cond cond))] + (table.insert match-body cond) + (table.insert match-body body))) + (if else-branch + (table.insert match-body else-branch)) + (match* val (unpack match-body)))) + {:-> ->* :->> ->>* :-?> -?>* :-?>> -?>>* :doto doto* :when when* :with-open with-open* :collect collect* :icollect icollect* :partial partial* :lambda lambda* :pick-args pick-args* :pick-values pick-values* :macro macro* :macrodebug macrodebug* :import-macros import-macros* - :match match*} + :match match**} diff --git a/test/macro.fnl b/test/macro.fnl index fa319df..76e08b6 100644 --- a/test/macro.fnl +++ b/test/macro.fnl @@ -130,17 +130,76 @@ "(match [:a [:b :c]] [a b :c] :no [:a [:b c]] c)" "c" "(match [:a {:b 8}] [a b :c] :no [:a {:b b}] b)" 8 "(match [{:sieze :him} 5] - ([f 4] ? f.sieze (= f.sieze :him)) 4 - ([f 5] ? f.sieze (= f.sieze :him)) 5)" 5 + ([f 4] ? f.sieze (= f.sieze :him)) 4 + ([f 5] ? f.sieze (= f.sieze :him)) 5)" 5 "(match nil _ :yes nil :no)" "yes" "(match {:a 1 :b 2} {:c 3} :no {:a n} n)" 1 "(match {:sieze :him} - (tbl ? (. tbl :no)) :no - (tbl ? (. tbl :sieze)) :siezed)" "siezed" + (tbl ? (. tbl :no)) :no + (tbl ? (. tbl :sieze)) :siezed)" "siezed" "(match {:sieze :him} - (tbl ? tbl.sieze tbl.no) :no - (tbl ? tbl.sieze (= tbl.sieze :him)) :siezed2)" "siezed2" - "(var x 1) (fn i [] (set x (+ x 1)) x) (match (i) 4 :N 3 :n 2 :y)" "y"}] + (tbl ? tbl.sieze tbl.no) :no + (tbl ? tbl.sieze (= tbl.sieze :him)) :siezed2)" "siezed2" + "(var x 1) (fn i [] (set x (+ x 1)) x) (match (i) 4 :N 3 :n 2 :y)" "y" + ;; New syntax -- general case + "(match [1 2 3 4] + 1 :nope1 + [1 2 4] :nope2 + (where [1 2 4]) :nope3 + (where (or [1 2 4] [4 5 6])) :nope4 + (where [a 1 2] (> a 0)) :nope5 + (where [a b c] (> a 2) (> b 0) (> c 0)) :nope6 + (where (or [a 1] [a -2 -3] [a 2 3 4]) (> a 0)) :success + :nope7)" :success + ;; Booleans are OR'ed as patterns + "(match false + (where (or false true)) :false + _ :nil)" :false + "(match true + (where (or false true)) :true + _ :nil)" :true + ;; Old syntax as well as new syntax + "(match [1 2 3 4] + (where (or [1 2 4] [4 5 6])) :nope1 + (where [a 2 3 4] (> a 10)) :nope2 + ([a 2 3 4] ? (> a 10)) :nope3 + ([a 2 3 4] ? (= a 1)) :success)" :success + "(match [1 2 3 4] + (where (or [1 2 4] [4 5 6])) :nope1 + (where [a 2 3 4] (> a 0)) :success1 + ([a 2 3 4] ? (> a 10)) :nope3 + ([a 2 3 4] ? (= a 1)) :success2)" :success1 + ;; nil matching + "(match nil + 1 :nope1 + 1.2 :nope2 + :2 :nope3 + \"3 4\" :nope4 + [1] :nope5 + [1 2] :nope6 + (1) :nope7 + (1 2) :nope8 + {:a 1} :nope9 + [[1 2] [3 4]] :nope10 + nil :success + :nope11)" :success + ;; no match + "(match [1 2 3 4] + (1 2 3 4) :nope1 + {:a 1 :b 2} :nope2 + (where [a b c d] (= 100 (* a b c d))) :nope3 + ([a b c d] ? (= 100 (* a b c d))) :nope4 + :success)" :success + ;; old tests adopted to new syntax + "(match [{:sieze :him} 5] + (where [f 4] f.sieze (= f.sieze :him)) 4 + (where [f 5] f.sieze (= f.sieze :him)) 5)" 5 + "(match {:sieze :him} + (where tbl (. tbl :no)) :no + (where tbl (. tbl :sieze)) :siezed)" :siezed + "(match {:sieze :him} + (where tbl tbl.sieze tbl.no) :no + (where tbl tbl.sieze (= tbl.sieze :him)) :siezed2)" :siezed2}] (each [code expected (pairs cases)] (l.assertEquals (fennel.eval code {:correlate true}) expected code)))) -- 2.29.2
This looks great; thanks! Andrey Listopadov <andreyorst@gmail.com> writes: