Author:
Serge Aleynikov <saleyn(at)gmail(dot)com>
Status:
Draft
Type:
Standards Track
Created:
09-Jun-2021
Erlang-Version:
R24

EEP 57: Extending the syntax of patterns with alternative matches #

Abstract #

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.

Specification #

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)

Choice of a Delimiter for Alternative Patterns #

In this specification we propose to use the pipe | symbol as the delimiter of alternative patters for the following reasons:

  1. It is already used for a similar reason in type specs (e.g. -spec f(foo|bar) -> ok.), and this natural choice of the delimiter would be consistent.
  2. It is familiar as the “or” condition to people coming from other languages.

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

Nested Alternative Patterns #

There are two approaches of dealing with the alternative patters:

  1. Permitting only the top-level alternation (e.g. Expr1 | Expr2 | ... | ExprN)
  2. Permitting unlimited nesting (e.g. {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.

Overlapping Patterns #

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

Rationale #

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.

Backwards Compatibility #

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.

Reference Implementation #

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

Copyright #

This document has been placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.