~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
1

[PATCH] implement new prefix syntax for match macro

Details
Message ID
<20210222102113.151545-1-andreyorst@gmail.com>
DKIM signature
pass
Download raw message
Patch: +148 -22
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
Details
Message ID
<87im6i1zst.fsf@whirlwind>
In-Reply-To
<20210222102113.151545-1-andreyorst@gmail.com> (view parent)
DKIM signature
missing
Download raw message
This looks great; thanks!

Andrey Listopadov <andreyorst@gmail.com> writes:

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

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

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

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

> +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
Reply to thread Export thread (mbox)