~technomancy/fennel

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
2 2

[PATCH v2] implement new prefix syntax for match macro

Details
Message ID
<20210223183557.201185-1-andreyorst@gmail.com>
DKIM signature
pass
Download raw message
Patch: +192 -19
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 an 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 moved old syntax to the Note 2 section in 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          | 67 +++++++++++++++++++++++++++++++++------
 src/fennel/macros.fnl | 71 +++++++++++++++++++++++++++++++++++++++--
 test/macro.fnl        | 73 ++++++++++++++++++++++++++++++++++++++-----
 3 files changed, 192 insertions(+), 19 deletions(-)

diff --git a/reference.md b/reference.md
index ac96573..295d35d 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,19 +393,66 @@ 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 after the pattern is a
condition; all the conditions must evaluate to true for that pattern
to match.

If several patterns share the same body and guards, such patterns can
be combined with `or` special in the `where` clause:

```fennel
(match [5 1 2]
  (where (or [a 3 9] [a 1 2]) (= 5 a)) "Will match either [5 3 9] or [5 1 2]"
  _ "will match anything else")
```

This is essentially equivalent to:

```fennel
(match [5 1 2]
  (where [a 3 9] (= 5 a)) "Will match either [5 3 9] or [5 1 2]"
  (where [a 1 2] (= 5 a)) "Will match either [5 3 9] or [5 1 2]"
  _ "will match anything else")
```

However, patterns which bind variables, should not be combined with
`or` if different variables are bound in different patterns or some
variables are missing:

``` fennel
;; bad
(match [1 2 3]
  ;; Will throw an error because `b' is nil for the first
  ;; pattern but the guard still uses it.
  (where (or [a 1 2] [a b 3]) (> a 0) (> b 1))
  :body)

;; ok
(match [1 2 3]
  (where (or [a b 2] [a b 3]) (> a 0) (>= b 1))
  :body)
```

**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 2:**: Prior to Fennel 0.8.2 the `match` macro used infix `?`
operator to test patterns against the guards. While this syntax is
still supported, `where` should be preferred instead:

``` fennel
(match [1 2 3]
  (where [a 2 3] (> a 0)) "new guard syntax"
  ([a 2 3] ? (> a 0)) "obsolete guard syntax")
```

### `global` set global variable

Sets a global variable to a new value. Note that there is no
diff --git a/src/fennel/macros.fnl b/src/fennel/macros.fnl
index 911a80d..99323ae 100644
--- a/src/fennel/macros.fnl
+++ b/src/fennel/macros.fnl
@@ -347,7 +347,9 @@ introduce for the duration of the body if it does match."
    syms))

(fn match* [val ...]
  "Perform pattern matching on val. See reference for details."
  ;; Old implementation of match macro, which doesn't directly support
  ;; `where' and `or'. New syntax is implemented in `match-where',
  ;; which simply generates old syntax and feeds it to `match*'.
  (let [clauses [...]
        vals (match-val-syms clauses)]
    ;; protect against multiple evaluation of the value, bind against as
@@ -355,10 +357,75 @@ 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) (= (. cond 1) `where))
      (let [second (. cond 2)]
        (if (and (list? second) (= (. second 1) `or))
            (transform-or second [(table.unpack cond 3)])
            :else
            [(list second '? (table.unpack cond 3))]))
      :else
      [cond]))

(fn match-where [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)"
  (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-where}
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
Details
Message ID
<87ft1m119o.fsf@whirlwind>
In-Reply-To
<20210223183557.201185-1-andreyorst@gmail.com> (view parent)
DKIM signature
missing
Download raw message
Thanks; applied and pushed!

Andrey Listopadov <andreyorst@gmail.com> writes:

> +(match data-expression
> +  pattern body
> +  (where pattern guard guards*) body
> +  (where (or pattern patterns*) guard guards*) body)"
> +  (let [conds-bodies (partition-2 (table.pack ...))

After I applied this I realized we can't use table.pack here because it
doesn't exist in older Lua versions. Luckily we can just use [...] and
instead of seq.n we can use the `length` operator, because ASTs are
guaranteed to not be sparse. When they look like they have nil in them,
they actually have a symbol named nil.

Anyway I've committed a fix and updated the changelog so we should be all set here.

-Phil
Details
Message ID
<CAAKhXoYiSbaj1=gX57csAQX7bwGi4MZgiESEOUJYAvR_qBVL9Q@mail.gmail.com>
In-Reply-To
<87ft1m119o.fsf@whirlwind> (view parent)
DKIM signature
pass
Download raw message
> After I applied this I realized we can't use table.pack here because it
> doesn't exist in older Lua versions.

oh, wasn't aware that table.pack is a new thing.

> When they look like they have nil in them,
> they actually have a symbol named nil.

BTW I've saw (= :nil (tostring pattern)) in the code of
src/fennel/macros.fnl at line 294. AST nils can't be compared to `nil
or is it just an oversight?


-Andrey
Reply to thread Export thread (mbox)