~technomancy/fennel#104: 
Nested matching

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.

Status
RESOLVED FIXED
Submitter
~technomancy
Assigned to
No-one
Submitted
3 years ago
Updated
3 years ago
Labels
enhancement next-release

~gandor 3 years ago

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 Haskell do block, or even the cited with 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))

~technomancy 3 years ago

It's true that making it work like let would make the pairing more obvious. But Clojure's when-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.

~technomancy 3 years ago

~technomancy 3 years ago

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

~technomancy 3 years ago

The problem with the name match-> is that all the other -> forms imply splicing, which is not present in this macro. Also the else clause is somewhat awkward here.

I've updated the branch in question to use the name match-try and replace else with catch. I think catch is much better as it implies it catches mismatches that happen in any step. I'm not so sold on match-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?

~gandor 3 years ago*

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 with catch. 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.

~technomancy 3 years ago

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.

~technomancy REPORTED FIXED 3 years ago

Merged in the branch containing this.

Register here or Log in to comment, or comment via email.