Hello everyone.
During yesterday's Fennel User Group meeting we discussed some ideas
about cleaning up the syntax for pattern matching when it comes to guard
clauses and `or` clauses. There was more or less broad agreement that
the current notation is not ideal:
(case (foo)
(where (or 1 2 3)) "a"
(where [x y z] (and ready? x)) [y z]
(where (or (x y) (y x)) (< x y)) "c"
_ :no-match)
The main reason is that patterns beginning with `where` look like a call
to a Fennel special form called "where", but this is not the case; you
can only use `where` inside pattern matching clauses. Even worse, `or`
patterns look like calls to Fennel's `or` special form, but are not that
at all; `(or 1 2 3)` in any other context is equivalent to just 1.
The simplest fix for this is to replace them with `&where` and `&or` to
emphasize the fact that they are not regular special forms but just
syntax introduced by the pattern matching macros. However, this does not
address the broader problem: ideally patterns should just be data;
anything else interferes with the declarative appeal of pattern matching.
Up to this point, I had only considered alternatives using different
syntax for the patterns themselves. But Rudolf suggested that it might
be possible to solve the problem by allowing for new forms on the right
hand side in the bodies corresponding to each pattern, which I'll refer
to as the "body-side" alternative:
(case (foo)
1 & 2 & 3 "a"
[x y z] (&if (and ready? x) [y z])
(x y) & (x _ y) (&if (< x y) "c")
_ :no-match)
This replaces `&or` in the pattern side with `&` as the body, which
causes that body to evaluate to the body for the next pattern. Rather
than using `&where` for guard clauses, we have a new `&if` form which is
like normal `&if`, but when the condition returns false/nil, rather than
the entire form returning nil, goes on to the next pattern instead. The
appeal of this style is that it allows the patterns to remain as pure
patterns, parentheses in patterns are used only to bind multiple values
and not to "call" anything to "construct" a pattern that isn't just
data. Also `&if` is very similar to the existing `if` form.
Finally we had one other suggestion by Andrey; if we require that the
pattern and the body have a separator between them, we can have a kind
of implicit `or` where any pattern before the separator may match. We
can include `&if` clauses as guards on the left hand side of the
separator as well. I don't think we can adopt this to the existing
`case` macro because it is backwards-incompatible, but it might look
like this, what I'm calling the "spread out" alternative:
(case+ (foo)
1 2 3 : "a"
[x y z] &if (and ready? x) : [y z]
(x y) (x _ y) &if (< x y) : "c"
_ : :no-match)
This also avoids making it look as if there is a pattern-specific kind
of "special form", and the `&->` and `&if` directives are just markers
for the things that come after them, which is more similar to the
existing `&as`, `&until`, etc directives we use already. However, it
sacrifices the regularity of the 1:1 relationship between pattern and
value which we see not only in `case` and `match` but also in `let` and
as such it feels less structured and more "infixy" like you would expect
to see in non-lisp languages.
In both these alternatives, the actual token used (`&`, `&if`, and `:`)
aren't necessarily fixed; if you really don't like the way `:` is used
in the last proposal for instance, that alone doesn't mean that proposal
should be rejected; replacing it with something like `&->` is very easy.
I'm interested in feedback from any Fennel users about these
alternatives. Try them out with some of your existing pattern matches
that have guards/or and see how they feel. What do you think?
-Phil
Phil Hagelberg <phil@hagelb.org> writes:
> During yesterday's Fennel User Group meeting we discussed some ideas> about cleaning up the syntax for pattern matching [...]
Thank you for a great summary, Phil!
> This replaces `&or` in the pattern side with `&` as the body, which> causes that body to evaluate to the body for the next pattern.
A slight correction is in order, as my proposal was simpler.
I meant for `&` to fall through to the next _pattern_, not to "evaluate
to the _body_ of the next pattern". In simple words, and combined with
Andrey's _fantastic_ `&if` idea, it now boils down to the following:
`&` is a _shorthand_ for `(&if false nil)`.
Hopefully, this sentence will finally make the idea clear!
Thus, `&` and `&if` are not _two_ different snowflakes but _one_. They
both exist for the same reason, namely to make the `case` or `match`
machinery "fall through" to the next pattern.
I _apologize_, once again and to everyone, for not communicating more
clearly! I am trying, folks.
Rudy
--
"Genius is 1% inspiration and 99% perspiration."
-- Thomas Alva Edison, 1932
Rudolf Adamkovič <salutis@me.com> [he/him]
Studenohorská 25
84103 Bratislava
Slovakia
Rudolf Adamkovič <salutis@me.com> writes:
> A slight correction is in order, as my proposal was simpler.
Actually, scratch that. It would not work, of course. Boy, am I
confused sometimes! Phil got it _perfectly_ right.
I apologize for the noise!
Rudy
--
"'Contrariwise,' continued Tweedledee, 'if it was so, it might be; and
if it were so, it would be; but as it isn't, it ain't. That's logic.'"
-- Lewis Carroll, Through the Looking Glass, 1871/1872
Rudolf Adamkovič <salutis@me.com> [he/him]
Studenohorská 25
84103 Bratislava
Slovakia