Currently Erlang only supports a single match in expression when defining a pattern match in case, receive expressions, function clauses, lambdas, try-of, and try-catch, the generators in list and bit-string comprehensions, and function arguments.
This proposal extends the syntax to allow multiple alternative matches to share the same body or to be matched against a right hand side expression.
Currently, a match in a case, receive, try-of expression looks similar to this syntax:
case I of
1 -> less_than_three;
2 -> less_than_three;
3 -> less_than_ten;
_ -> other
end.
This should be changed to allow:
case I of
1 | 2 -> less_than_three;
3 -> less_than_ten;
_ -> other
end.
Similarly, a function or an expression match looks like:
foo(1) -> ok;
foo(2) -> ok;
foo(N) -> other.
More generally, a pattern in a case, receive, try-of expression, lambda, or a pattern match, should be extended from the following:
case Expr of
Pattern1 [when GuardSeq1] ->
Body1;
...;
PatternN [when GuardSeqN] ->
BodyN
end
receive
Pattern1 [when GuardSeq1] ->
Body1;
...;
PatternN [when GuardSeqN] ->
BodyN
after ExprT ->
BodyT
end
try Exp [of
Pattern1 [when GuardSeq1] ->
Body1;
...;
PatternN [when GuardSeqN] ->
BodyN
]
catch
Class1:ExceptionPattern1[:Stacktrace] [when ExceptionGuardSeq1] ->
ExceptionBody1;
...;
ClassN:ExceptionPatternN[:Stacktrace] [when ExceptionGuardSeqN] ->
ExceptionBodyN
end
Res = ExprF(Expr1,...,ExprN)
to support the following syntax:
case Expr of
Pattern1A |
Pattern1B |
...
Pattern1N [when GuardSeq1] ->
Body1;
...;
PatternNA |
PatternNB |
...
PatternNN [when GuardSeqN] ->
BodyN
end
receive
Pattern1A |
Pattern1B |
...
Pattern1N [when GuardSeq1] ->
Body1;
...;
PatternNA |
PatternNB |
...
PatternNN [when GuardSeqN] ->
BodyN
after ExprT ->
BodyT
end
try Exp [of
Pattern1A |
Pattern1B |
...
Pattern1N [when GuardSeq1] ->
Body1;
...;
PatternNA |
PatternNB |
...
PatternNN [when GuardSeqN] ->
BodyN
]
catch
Class1:ExceptionPattern1A|
ExceptionPattern2A| ...
ExceptionPatternNA[:StackTrace] [when ExceptionGuard1A] ->
ExceptionBodyE1;
ClassN:ExceptionPattern1N|
ExceptionPattern2N| ...
ExceptionPatternNN[:StackTrace] [when ExceptionGuardNN] ->
ExceptionBodyEN
end
Res1 | Res2 | ... | ResN =
ExprF(Expr1, Expr2, ... , ExprN)
ExprF1(Expr1A1 | Expr1A2 | ... | Expr1AN, ...,
Expr1N1 | Expr1N2 | ... | Expr1NN) [when GuardSeq1] -> Body1;
ExprFN(ExprNA1 | ExprNA2 | ... | ExprNAN, ...,
ExprNN1 | ExprNN2 | ... | ExprNNN) [when GuardSeq1] -> Body1
A variable can only be used in the guards if its bound in all alternative patterns. That is, the following is allowed:
case X of
{A, 0} | {0, A} when A > 0 -> ok
end
{A, 1} | {A, 2} = foo()
bar({A, 1} | {A, 2}) -> true
and the following is not allowed:
case X of
{A, 0} |
{0, B} when B > 0 -> ok
end
{A, 1} | {B, 2} = foo()
bar({A, 1} | {B, 2}) -> true
The cases shown above would produce a compilation error in the form:
Error: alternative patterns must have the same variables defined
In cases when the use of |
becomes ambiguous, such as when using
in combination with cons in lists, the precedence is given to the
legacy cons syntax, and parenthesis can be used to disambiguate the
alternative match expressions:
case X of
[3 | T] -> {ok, T};
[1 | 2 | T] -> error; % Compiler error: ambiguous use of pipe symbol
[(1|2) | T] ->
% Alternative matches, of lists' head being 1 or 2
{ok, T}
end
[(1|2) | T] = check_head_version(L)
In this specification we propose to use the pipe |
symbol as the
delimiter of alternative patters for the following reasons:
-spec f(foo|bar) -> ok.
), and this natural choice of the
delimiter would be consistent.A possible alternative would be to use the ;
delimiter, which
presently has a similar meaning as the or
condition separating terms
in guards, “if” expressions, and list comprehensions.
However, the semicolon (;
) could create confusion in identifying
the end of the pattern in a statement or in separating expressions. E.g.:
case Expr of
a ->
true = foo(); % <-- is this the end of the body or a syntax error
% which should be a comma, with the body
% returning 'b'?
b; % <-- is this the alternative pattern or a pattern
% with a missing body?
C when is_integer(C) ->
false
end
Additionally, ;
is used to separate clauses in if, case, receive,
try-of-catch, fun and functions. This would potentially create more
confusion if it’s selected as the delimiter of alternative patterns.
Another possible alternative would be to allow to skip bodies in the if, case, recieve, try-of and fun expressions:
case Expr of
a ->
b ->
c ->
true;
_ ->
false
end
However, the application of the same principle for representing alternative patterns in matching returns of function calls would cause more confusion:
a -> b -> c = foo()
There are two approaches of dealing with the alternative patters:
Expr1 | Expr2 | ... | ExprN
){a|{b|c, d}} | e | {f|g, [(h|i), (j|k) | [(l|m)]]}
)While supporting the nested patterns is desirable, depending on the complexity of implementation, the unlimited nesting may be decided not to be supported. More specifically, it may turn out that the complexity of implementing nested patterns by the compiler outweighs the benefits of this feature.
In cases when alternative patterns overlap, the natural left-to-right
evaluation order would be used similar to the order of patterns in
a case
statement. E.g.
{a, X} | {X, b} = foo()
This example would be equivalent to:
case foo() of
Term = {a, X} -> Term;
Term = {X, b} -> Term;
Term -> error({badmatch, Term})
end
Implementation of this proposal would lead to a more convenient way to code redundant matching clauses, and would eliminate code duplication.
Example of where this could be useful can be found in many places in the existing OTP code. E.g. ssl_logger.erl:
case logger:compare_levels(Level, debug) of
lt ->
?LOG_DEBUG(#{direction => Direction,
protocol => Protocol,
message => Message},
#{domain => [otp,ssl,Protocol]});
eq ->
?LOG_DEBUG(#{direction => Direction,
protocol => Protocol,
message => Message},
#{domain => [otp,ssl,Protocol]});
_ ->
ok
end.
case logger:compare_levels(Level, notice) of
lt ->
?LOG_NOTICE(Report);
eq ->
?LOG_NOTICE(Report);
_ ->
ok
end.
Additionally, this syntax extension leads to consistency between function spec definitions and their implementations:
-spec f(foo|bar) -> ok.
f(foo|bar) -> ok.
Any code that uses the legacy implementation of the case and receive expressions will continue to work as it does today and produce the same results.
As an implementation suggestion, the AST can be rewritten by to duplicate
the alternative patterns containing the pipe, so the case of
X of lt | gt -> ok end
becomes case X of lt -> ok; gt -> ok end
.
For match expressions, it could rewrite lt | gt = compare(A, B)
to a case statement case compare(A, B) of lt -> lt; gt -> gt end
,
and nested alternative patterns could use guards for rewriting matching,
e.g.:function argument matches foo(lt | gt, a | b) -> true
would become:
foo(lt | gt, a | b) -> true
becomes:
foo(A, B) ->
case A of
_ when A =:= lt; A =:= gt ->
case B of
_ when B =:= a; B =:= b ->
. . .
end
end
Rewriting alternative patterns in list comprehensions by the compiler presents an additional challenge. Implementations suggestions include the following:
[X || {a,X} | {b,X} <- L]
becomes:
[case I of
{a, X} | {b, X} -> X
end
|| I <- L,
case I of
{a, _} | {b, _} -> true;
_ -> false
end
]
or:
[X || {X} <-
[case Item of
{a,X0} | {b,X0} ->
{X0}; % Set of matched variables
_ ->
nomatch
end || Item <- L]]
This document has been placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.