One common pattern with pattern matching is that you "drill down" thru a number of steps which could possibly fail and the result of one pattern match becomes the input to another. This is very awkward right now in Fennel; you end up with something like:
(match (get-file-name-for user)
filename (match (io.open filename :w)
f (match (f:write data)
f (notify user)
(_ msg) (print "could not write data"))
(_ msg) (print "could not open file" msg))
_ (print "no file for this user"))
Rather than requiring this degree of nesting, we should consider an alternate matching form which can allow for the value from each clause to be matched by the next. Perhaps something like this:
(match-try (get-file-name-for user)
filename (io.open filename :w)
file (file:write data)
file (notify user))
Normally if at any stage it does not match, the entire form returns the value (or values) which don't match. But you can add a catch
clause to gather all the mismatch handling together in one place:
(match-try (conn:receive :*l)
input (parse input)
(command-name params) (commands.get command-name)
command (pcall command (table.unpack params))
(catch
(_ :timeout) nil
(_ :closed) (pcall disconnect conn "connection closed")
(_ msg) (print "Error handling input" msg)))
This is similar to the with
form in Elixir: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1
Originally this was proposed with the match->
name, but that is misleading because it does not do any splicing like ->
does. match-try
has the advantage of making it clearer what the catch
clause is for, but it kind of implies that there is some dynamic scoping of error handling (the way try works in other languages) which does not happen here; this only works in terms of return values. Other proposals: match-steps
, match>
, and match-staged
.
Something like this would indeed be a great addition, but the diagonal pairing without nesting makes the suggested form a bit surprising, and harder to understand intuitively than the usual
when-let*
macro from Lisp/Clojure folklore. There's nothing inherently wrong with it I guess, except for going against convention, but any such maybe monad equivalents that come to mind - a Haskelldo
block, or even the citedwith
form of Elixir - are usually arranged as pairs of bindings stacked on top of each other:(when-let [filename (get-file-name-for user) file (io.open filename :w) file (file:write data)] (notify user))
It's true that making it work like
let
would make the pairing more obvious. But Clojure'swhen-let
does not work the way you described; it specifically disallows having multiple bindings because (according to Rich) it would be unclear whether they would be ANDed or ORed together.Here's an example which makes it more let-like:
(match-let [{: filename} (get-file-info-for user) file (io.open filename :w) file (file:write data)] (notify user))The disadvantage here is that it this is less clear about what would happen in the case of the match failing. The semantics we want are for the body to be returned (so the nil+error-msg value can propagate) but it's not clear at all by looking at it that the body will not get evaluated in that case.
The match-> arrow form (by analogy to -?>) is a lot clearer about that.
Posted an initial proposal here: https://git.sr.ht/~technomancy/fennel/commit/match-arrow
I've added support for an
else
clause which can be used as a single place to do all your error handling if there is a mismatch:(match-> (hello) {: a : b} (process-ab a b) [one two & rest] (keep-processing (+ one two) rest) {: result} (print result) (else [] (print "Not enough results to continue.") (nil msg) (print "Could not process!" msg)))
The problem with the name
match->
is that all the other->
forms imply splicing, which is not present in this macro. Also theelse
clause is somewhat awkward here.I've updated the branch in question to use the name
match-try
and replaceelse
withcatch
. I thinkcatch
is much better as it implies it catches mismatches that happen in any step. I'm not so sold onmatch-try
but it's OK.I have found that when I'm explaining this macro, I keep coming back to saying "sequence of steps" as I try to describe what it's for. So maybe
match-steps
would be a good name for it?
The problem with the name match-> is that all the other -> forms imply splicing, which is not present in this macro.
Yes, that is definitely confusing. Another thing to keep in mind is that currently all similar short-circuiting forms tend to have a
?
in their name, which is a good convention in my opinion. Would it be a bad idea to simply call this form e.g.match-?
? (match?
looks too much like a predicate function.) On the other hand,try
pairs nicely withcatch
. These names, however, are a bit more obscure in that they do not imply sequencing.I think catch is much better as it implies it catches mismatches that happen in any step.
Exactly.
Another thing to keep in mind is that currently all similar short-circuiting forms tend to have a
?
in their name, which is a good convention in my opinion.That's an interesting position. The intent with such forms was actually not so much that they short-circuit, but that they are nil-safe. The question mark was chosen specifically because it matches the convention to name variables ?foo to warn of them possibly being nil. That's why the nil-safe field accessor is ?. and not .? as the latter would imply it is a predicate function.
Merged in the branch containing this.