~technomancy/fennel

implement new prefix syntax for match macro v1 PROPOSED

Andrey Listopadov: 1
 implement new prefix syntax for match macro

 3 files changed, 148 insertions(+), 22 deletions(-)
Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git
View this thread in the archives

[PATCH] implement new prefix syntax for match macro Export this patch

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.
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.

### `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

(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))
            (transform-or second [(table.unpack cond 3)])
            :else
            [(list second '? (table.unpack cond 3))]))
      :else
      [cond]))

(fn match** [val ...]
  "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 [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: