This is a proposal for the native implementation of a match statement
for pattern matching in julia. This was previously discussed in
JuliaLang/julia#18285 and there have been
various packages for this as well, most notably https://github.com/JuliaServices/Match.jl. `match is also found in several other languages, such as Scala
(https://docs.scala-lang.org/tour/pattern-matching.html) and Rust (https://doc.rust-lang.org/book/ch06-02-match.html) as well as many functional
progamming languages
This proposal is my attempt at a native julian take on these constructs, so the semantics are not exactly identical to any of these, but they should be considered inspiration of course.
The control flow of a match statement is essentially an if/elseif change,
evaluated top to bottom, combined with automatic destructuring.
For example, consider:
print(match n
1 => "Hello"
2 => "World"
_ => "Other"
end)
This is semantically equivalent to writing:
print(if n == 1
"Hello"
elseif n == 2
"World"
else
"Other"
end)
On the other hand, match also supports destructuring:
zero = 0
match (x, y)
(1, a) | (a, 1) => 1 + a
($zero, b) | (b, $zero) => 2 + b
_ => 0
end
We will call each of these statements a match case and the
LHS of these expressions as a match pattern.
Match patterns generalize ordinary destructuring assigment.
Falling through to the end of a match statement is a runtime error.
Continuing by example, the above example would expand to:
zero = 0
matchee = (x, y)
pat1 = matcher(|, matcher(tuple, 1, Capture(1)),
matcher(tuple, Capture(1), 1))
r = match(pat1, matchee)
if r !== nothing
(a, ) = r
1 + a
else
pat2 = matcher(|,
matcher(tuple, zero, Capture(1)),
matcher(tuple, Capture(1), zero))
r = match(pat2, patchee)
if r !== nothing
(b, ) = r
2 + b
else
0
end
end
There are some subtle semantics here:
-
Any identifier occuring in other-than-call position get replaced by
Capture. -
Any call expressions become calls to
matcher. Note that the identifier of the call is evaluated, not replaced by Capture. -
Any variable or expression can be explicitly pasted into non-call position by escaping with
$.
In some situations, the destructuring offered by match may be more powerful than
ordinary destructuring assignment. We cannot simply extend all destructuring patterns
to the LHS of ordinary assignment - this would conflict with function declaration.
Instead, there is a special match destructuring assignment syntax like so:
match (a, b) = val
This assigns a, b in toplevel scope, but the LHS of the = is parsed as a match
pattern and no end is required.
Like Rust, Match.jl, and generators, if guards are available:
match val
(a, b) if sin(b) == 0. => a
(a, b) => b
end
A single _ in match case position is equivalent to x=>x, i.e.:
v = match foo()
x if x == 0 => error()
_
end
Assigns v to the result of foo in the fallback case.
match is currently used as a funciton name for regex matching. In addition, it is
a not-uncommon variable name. We should keep these working to the extent possible. As
such, match is not a keyword if it occurs (without whitespace) before ( or = or
as a single identifier in any other context.
match is generally parsed as a macro call to the @match macro with all
the smarts in the macro implementation or the match and matcher runtime functions.
However, there are some parsing differences as well:
-
Any expression other than
=>is disallowed. -
Plain
::ais allowed and corresponds totypeassert(a), i.e. gets lowered tomatcher(typeassert, a) -
The special if guard syntax above is available