~technomancy/fennel

3 2

Prefix notation for match guards

Details
Message ID
<CAAKhXoZvLEdrmfnK+th4vQHwQF0=bPtP+Lt3GFOBsR+n1i8-aw@mail.gmail.com>
DKIM signature
pass
Download raw message
After recent discussion on IRC about ne match syntax proposals, I've
experimented with infix notation, and here's my results so far:

   (match2 [10 20 30]
     [a 2 3]
     "matches [non-nil 2 3]"
     (where [a 2 3] (> a 20))
     "matches [num>20 2 3]"
     (where (or [_ 2 c] [1 _ c]) (> c 30))
     "matches [anything 2 num>30] or [1 anything num>30]"
     "executed when everything before failed")

This currently expands to inbuilt match macro:

   (match [10 20 30]
     [a 2 3]
     "matches [non-nil 2 3]"
     ([a 2 3] ? (> a 20))
     "matches [num>20 2 3]"
     ([_ 2 c] ? (> c 30))
     "matches [anything 2 num>30] or [1 anything num>30]"
     ([1 _ c] ? (> c 30))
     "matches [anything 2 num>30] or [1 anything num>30]"
     "executed when everything before failed")

You can see that `match2` had 4 branches, and resulting `match` has 5
branches. This is because one branch in `match2` had `or` operator,
which allows for guard to be applied to several patterns, and if any
pattern matches the body will be executed.

Because it is a simple transformation to inbuilt `match` everything
`match` supports is supported by `match2` macro.

Below is the implementation. It's not very big and contains some
simple list transformations. This is not a proposed implementation,
rather an example to play with. Happy to hear any thoughts on this
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 []]
   (for [i 1 seq.n 2]
     (table.insert firsts (or (. seq i) 'nil)))
   (for [i 2 seq.n 2]
     (table.insert seconds (or (. seq i) 'nil)))
   (icollect [i v1 (ipairs firsts)]
     (let [v2 (. seconds i)]
       (when v2
         [v1 v2])))))

(fn transform-or [[_ & pats] guards]
 "Transforms `(or pat pats*)` lists into match `guard` patterns.

Input:  (or pat1 pat2), guard
Output: [(pat1 ? guard) (pat2 ? guard)]"
 (icollect [_ pat (ipairs pats)]
   (list pat '? (table.unpack guards))))

(fn transform-cond [cond]
 "Transforms `where` cond into sequence of `match` guards.

Input:  pat
Output: [pat]

Input:  (where pat guard)
Output: [(pat ? guard)]

Input:  (where (or pat1 pat2) guard)
Output: [(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))]))
     [cond]))

(fn match2 [val ...]
 "Match like macro with prefix notation for guards and ability to
specify several patterns for one body.

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 (when (not= 0 (% (select :# ...) 2))
                     (select (select :# ...) ...))
       match-body (list 'match val)]
   (each [_ [cond body] (ipairs conds-bodies)]
     (each [_ cond (ipairs (transform-cond cond))]
       (table.insert match-body cond)
       (table.insert match-body body)))
   (when else-branch
     (table.insert match-body else-branch))
   (print (.. ";; " (view match-body)))
   match-body))

{: match2}

-- 
Best regards,
Andrey Listopadov
Details
Message ID
<87lfbt9q9p.fsf@whirlwind>
In-Reply-To
<CAAKhXoZvLEdrmfnK+th4vQHwQF0=bPtP+Lt3GFOBsR+n1i8-aw@mail.gmail.com> (view parent)
DKIM signature
missing
Download raw message
Andrey Orst <andreyorst@gmail.com> writes:

> After recent discussion on IRC about ne match syntax proposals, I've
> experimented with infix notation, and here's my results so far:
>
>    (match2 [10 20 30]
>      [a 2 3]
>      "matches [non-nil 2 3]"
>      (where [a 2 3] (> a 20))
>      "matches [num>20 2 3]"
>      (where (or [_ 2 c] [1 _ c]) (> c 30))
>      "matches [anything 2 num>30] or [1 anything num>30]"
>      "executed when everything before failed")

This is great; I like how `where` reminds one of its usage in SQL or
mathematics. I was originally a little hesitant to use `or` here because
of the following construct:

    (let [x false]
      (match false
        (or x [y z]) :hello))

In this case it behaves differently from normal `or`, but I think it's
clear enough that it's the *matchiness* being ORed, not the *value* as
it is with normal `or`. But what do others think?

-Phil
Details
Message ID
<CAAKhXoZ+CGb4M5Jh-5ug9-OgHW2QLcRd7XFXj5JE5fxy05+Yfg@mail.gmail.com>
In-Reply-To
<87lfbt9q9p.fsf@whirlwind> (view parent)
DKIM signature
pass
Download raw message
> This is great; I like how `where` reminds one of its usage in SQL or
> mathematics. I was originally a little hesitant to use `or` here because
> of the following construct:
>
>     (let [x false]
>       (match false
>         (or x [y z]) :hello))
>
> In this case it behaves differently from normal `or`, but I think it's
> clear enough that it's the *matchiness* being ORed, not the *value* as
> it is with normal `or`. But what do others think?

So if I understood you correctly, you're OK with allowing `or` outside
of `where` clause?  My initial implementation only allowed it inside
`where` specifically to indicate that this `or` is not ordinary `or`,
but a part of `where` syntax.  This in turn meant that all match
clauses which don't have guards, but have altering patterns would
require `where` and `or` like this:

    (match expr
      (where (or pat1 pat2 pat3)) :common-body)

But I see you're using `or` as is:

    (match expr
      (or pat1 pat2 pat3) :common-body)

I guess this is fine too, as `match` itself can override `or` for
special meaning, without relying on `where` clause, so I just need a
confirmation here, that this is the way we want to go.

Another question is backwards compatibility.

Do we want to keep `(pat ? guard)` syntax around for some time and
warn users that it will be deprecated in the future?  I think that
this should be done, as match is pretty versatile macro, and such
drastic syntax change can break a lot of code.  Below is the updated
implementation of `match2` which allows `or` outside of `where`, so
you could play with it and see if you like such syntax or the older
one more.

(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 []]
    (for [i 1 seq.n 2]
      (table.insert firsts (or (. seq i) 'nil)))
    (for [i 2 seq.n 2]
      (table.insert seconds (or (. seq i) 'nil)))
    (icollect [i v1 (ipairs firsts)]
      (let [v2 (. seconds i)]
        (when v2
          [v1 v2])))))

(fn transform-or [[_ & pats] guards]
  ;; Transforms `(or pat pats*)` lists into match `guard` patterns.
  ;;
  ;; Input:  (or pat1 pat2), guard
  ;; Output: [(pat1 ? guard) (pat2 ? guard)]
  (icollect [_ pat (ipairs pats)]
    (list pat '? (table.unpack guards))))

(fn transform-cond [cond]
  ;; Transforms `where` cond into sequence of `match` guards.
  ;;
  ;; Input:  pat
  ;; Output: [pat]
  ;;
  ;; Input:  (or pat1 pat2)
  ;; Output: [pat1 pat2]
  ;;
  ;; Input:  (where pat guard)
  ;; Output: [(pat ? guard)]
  ;;
  ;; Input:  (where (or pat1 pat2) guard)
  ;; Output: [(pat1 ? guard) (pat2 ? guard)]
  (if (list? cond)
      (if (= (tostring (. cond 1)) :or)
          [(table.unpack cond 2)]
          (= (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))])))
      [cond]))

(fn match2 [val ...]
  "Match like macro with prefix notation for guards and ability to
specify several patterns for one body.

Syntax:

(match data-expression
  pattern body
  (or pattern patterns*) body
  (where pattern guard guards*) body
  (where (or pattern patterns*) guard guards*) body
  else-body)"
  (let [conds-bodies (partition-2 (table.pack ...))
        else-branch (when (not= 0 (% (select :# ...) 2))
                      (select (select :# ...) ...))
        match-body (list 'match val)]
    (each [_ [cond body] (ipairs conds-bodies)]
      (each [_ cond (ipairs (transform-cond cond))]
        (table.insert match-body cond)
        (table.insert match-body body)))
    (when else-branch
      (table.insert match-body else-branch))
    (print (.. ";; " (view match-body)))
    match-body))

{: match2}


-- 
Best regards,
Andrey Listopadov
Details
Message ID
<87eeh9kyhq.fsf@whirlwind>
In-Reply-To
<CAAKhXoZ+CGb4M5Jh-5ug9-OgHW2QLcRd7XFXj5JE5fxy05+Yfg@mail.gmail.com> (view parent)
DKIM signature
missing
Download raw message
Andrey Orst <andreyorst@gmail.com> writes:

> So if I understood you correctly, you're OK with allowing `or` outside
> of `where` clause?  My initial implementation only allowed it inside
> `where` specifically to indicate that this `or` is not ordinary `or`,
> but a part of `where` syntax.

Oh interesting; I guess I overlooked this aspect of your proposal.

Honestly I don't feel strongly either way. Like... the advantage of your
original proposal is that it limits the surface area of what's
"special". You look for `where` and if it's not there then the entire
pattern is treated literally.

The other advantage of requiring `or` to be inside `where` is that if we
go that route for a while and try it out, and we decide that top-level
`or` would be very nice to have, we can add it in later. We can't go the
other way and remove it if we decide after a while that we don't like it
after putting it in.

Because of this I'm inclined to go with your original proposal where
`or` can't be used except inside `where`.

> Another question is backwards compatibility.
>
> Do we want to keep `(pat ? guard)` syntax around for some time and
> warn users that it will be deprecated in the future?  I think that
> this should be done, as match is pretty versatile macro, and such
> drastic syntax change can break a lot of code.

We need to keep the ? style of guards around for sure. I don't know if
we should issue a deprecation warning for the old style... at this point
it might never be removed; we just need to update the docs to recommend
the new way, similar to the situation with require-macros.

I feel that the harm in keeping it around is minimal when compared
to the breakage that comes from removing it.

-Phil
Reply to thread Export thread (mbox)