Reference material on pattern matching precedence order?

Hey guys, anyone has any references/docs about pattern matching precedence order?

I mean

iex> a_and_b = %{a: "A"} = %{a: "A", b: "B"}
> %{a: "A", b: "B"}

is done right to left, right?

So

iex> a_and_b
> %{a: "A", b: "B"}

But what if I wanted to do

iex> (a_and_b = %{a: "A"}) = %{a: "A", b: "B"}  # note the parens
> %{a: "A", b: "B"}

For me it evaluates the same, as if there weren’t any parens…

Now I can’t be sure but consider this:

iex(1)> (a_and_b = %{a: "A"}) = %{a: "A", b: "B"}
%{a: "A", b: "B"}
iex(2)> a_and_b
%{a: "A", b: "B"}
iex(3)> fun = fn(a_and_b = %{a: "A"}) -> a_and_b end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(4)> fun.(%{a: "A", b: "B"})
%{a: "A", b: "B"}
iex(5)>

Given the observed behaviour it seems that adding the parentheses isn’t overriding the right associativity of the match operator but instead is turning (a_and_b = %{a: "A"}) into a full pattern, so:

  • %{a: "A", b: "B"} is still first matched to %{a: "A"}
  • and once successful, the result is bound to a_and_b.
iex(5)> (a_and_b = %{a: "A"}) = %{a: "A", b: "C"}   
%{a: "A", b: "C"}
iex(6)> a_and_b                                     
%{a: "A", b: "C"}
iex(7)> (a_and_b = %{a: "A"}) = %{a: "B", b: "C"}
** (MatchError) no match of right hand side value: %{a: "B", b: "C"}

iex(7)> a_and_b                                     
%{a: "A", b: "C"}
iex(8)> 

Unfortunately the AST simply reflects the syntax (i.e. the pattern(s) aren’t identified at this point)

iex(8)> ast = quote do: a_and_b = %{a: "A"} = %{a: "A", b: "B"}  
{:=, [],
 [
   {:a_and_b, [], Elixir},
   {:=, [], [{:%{}, [], [a: "A"]}, {:%{}, [], [a: "A", b: "B"]}]}
 ]}
iex(9)> ast = quote do: (a_and_b = %{a: "A"}) = %{a: "A", b: "B"}
{:=, [],
 [
   {:=, [], [{:a_and_b, [], Elixir}, {:%{}, [], [a: "A"]}]},
   {:%{}, [], [a: "A", b: "B"]}
 ]}
iex(10)> 

Another experiment:

% file: demo.erl
-module(demo).
-export([demo1/0,demo2/0]).

demo1() ->
    A_and_B = #{a := "A"} = #{a => "A", b => "B"},
    A_and_B.

demo2() ->
    (A_and_B = #{a := "A"}) = #{a => "A", b => "B"},
    A_and_B.
1> c("demo.erl").
{ok,demo}
2> demo:demo1().
#{a => "A",b => "B"}
3> demo:demo2().
#{a => "A",b => "B"}
4> c("demo.erl",'S').
** Warning: No object file created - nothing loaded **
ok
5> 

File: demo.S

{module, demo}.  %% version = 0

{exports, [{demo1,0},{demo2,0},{module_info,0},{module_info,1}]}.

{attributes, []}.

{labels, 11}.


{function, demo1, 0, 2}.
  {label,1}.
    {line,[{location,"demo.erl",5}]}.
    {func_info,{atom,demo},{atom,demo1},0}.
  {label,2}.
    {move,{literal,#{a => "A",b => "B"}},{x,0}}.
    {get_map_elements,{f,3},{x,0},{list,[{atom,a},{x,1}]}}.
    {test,is_eq_exact,{f,3},[{x,1},{literal,"A"}]}.
    return.
  {label,3}.
    {line,[{location,"demo.erl",6}]}.
    {badmatch,{x,0}}.


{function, demo2, 0, 5}.
  {label,4}.
    {line,[{location,"demo.erl",9}]}.
    {func_info,{atom,demo},{atom,demo2},0}.
  {label,5}.
    {move,{literal,#{a => "A",b => "B"}},{x,0}}.
    {get_map_elements,{f,6},{x,0},{list,[{atom,a},{x,1}]}}.
    {test,is_eq_exact,{f,6},[{x,1},{literal,"A"}]}.
    return.
  {label,6}.
    {line,[{location,"demo.erl",10}]}.
    {badmatch,{x,0}}.


{function, module_info, 0, 8}.
  {label,7}.
    {line,[]}.
    {func_info,{atom,demo},{atom,module_info},0}.
  {label,8}.
    {move,{atom,demo},{x,0}}.
    {line,[]}.
    {call_ext_only,1,{extfunc,erlang,get_module_info,1}}.


{function, module_info, 1, 10}.
  {label,9}.
    {line,[]}.
    {func_info,{atom,demo},{atom,module_info},1}.
  {label,10}.
    {move,{x,0},{x,1}}.
    {move,{atom,demo},{x,0}}.
    {line,[]}.
    {call_ext_only,2,{extfunc,erlang,get_module_info,2}}.

Doesn’t look like those parens had any impact on demo2 whatsoever (it looks like A_and_B was optimized right out because it wasn’t doing anything anyway).


Finally:

Match Operator = in Patterns
If Pattern1 and Pattern2 are valid patterns, then the following is also a valid pattern:

Pattern1 = Pattern2

When matched against a term, both Pattern1 and Pattern2 will be matched against the term. The idea behind this feature is to avoid reconstruction of terms.


So in your example

a_and_b = %{a: "A"} = %{a: "A", b: "B"}

can be abstracted as

Pattern2 = Pattern1 = Term

while

(a_and_b = %{a: "A"}) = %{a: "A", b: "B"}

can be abstracted as

(Pattern2 = Pattern1) = Term

according to the above rule both Pattern2 and Pattern1 will be matched against Term (whether there are parens or not).

Note that in Erlang there is a clear differentiation between a map pattern, e.g. #{a := "A"} versus a map as a term , e.g. #{a => "A", b => "B"}; in Elixir they look the same, e.g. %{a: "A"} and %{a: "A", b: "B"} so that may make matters a bit more confusing.

4 Likes

It makes sense. Thank you for your help!

Thus:

iex(16)> %{c: "C"} = %{a: "A"} = %{a: "A", b: "B", c: "C"}
%{a: "A", b: "B", c: "C"}

iex(17)> (%{c: "C"} = %{a: "A"}) = %{a: "A", b: "B", c: "C"}
%{a: "A", b: "B", c: "C"}

iex(18)> (%{c: c} = %{a: a}) = %{a: "A", b: "B", c: "C"}    
%{a: "A", b: "B", c: "C"}
iex(19)> c
"C"
iex(20)> a
"A"

Saying “both Pattern2 and Pattern1 will be matched against Term (whether there are parens or not)” is functionally equivalent to saying “the parens are ignored, the associativity is always from right”. For Pattern2 will be matched against Term, resulting in Term. Then Pattern1 will be matched against the result, that is Term again.

That isn’t how I read

both Pattern1 and Pattern2 will be matched against the term.

to me it implies in

Pattern0 = ... = PatternN = Term

Term is matched against all {Pattern0,..,PatternN}, i.e. between all the patterns no associativity of any kind comes into play - each pattern is matched against the term individually and if any one of them fails the whole match fails.

2 Likes

Yes, this is how it works. It means that each pattern will be matched against the term. There is an implicit grouping (Pattern0 = (Pattern2 = … (PatternN = Term)…)) so if variables occur in many the binding behaves as the matching goes from the inside out. Fro example:

iex(1)> {a,b} = {b,a} = {3,4}
{3, 4}
iex(2)> a
3
iex(3)> b
4
iex(4)> {b,a} = {a,b} = {3,4}
{3, 4}
iex(5)> a
4
iex(6)> b
3

If you pin (’^’ ) a variable in any of the patterns then this importing of the value occurs before any matching:

iex(7)> {^x,y} = {y,x} = {3,4}
** (CompileError) iex:7: unbound variable ^x
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
iex(7)> {x,y} = {y,^x} = {3,4}
** (CompileError) iex:7: unbound variable ^x
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    (stdlib) lists.erl:1355: :lists.mapfoldl/3

(Erlang does not have this “issue” of ordering as variables can’t be rebound.)

6 Likes

Thanks for the clarification.

It raised one more question, though.

Interestingly, playing with your examples shows the parenthesis do make a difference:

iex(1)> {a,b} = {b,a} = {3,4}
{3, 4}
iex(2)> a
3
iex(3)> b
4

but

iex(1)> ({a,b} = {b,a}) = {3,4}
** (MatchError) no match of right hand side value: {3, 4}

So in the last example seems {a,b} = {b,a} is evaluated first, like ((Pattern0 = Pattern1) = Term)

Yes, it seems like the RHS of a = expression must be a term. Now the value of a = pattern match is the RHS so it works going right-to-left. Pinning is done first.

Hmm …

iex(1)> ({a,b} = {b,a}) = {3,4}
** (MatchError) no match of right hand side value: {3, 4}

iex(1)> ({a,b} = {b,a}) = {3,3}
{3, 3}
iex(2)> {b,a} = ({a,b} = {a,c}) = {3,4}
{3, 4}      
iex(3)> a
4
iex(4)> b
3
iex(5)> ({b,a} = {a,b}) = {a,c} = {3,4}
** (MatchError) no match of right hand side value: {3, 4}
 
iex(5)> ({a,b} = {a,c}) = {b,a} = {3,4}
{3, 4}
iex(6)> a
3
iex(7)> b
4
iex(8)> 

Pinning is done first.

So essentially the parentheses are disallowing the re-binding that would happen in their absence.

The parentheses are effectively pinning all bindings to their initial value (inside the parentheses). I’m simply re-emphasizing this because up to now I’ve only encountered the pinning concept in connection with Kernel.^/1 - this is the first time that I’ve seen parentheses having a similar effect.

The order of evaluation (right-to-left) seems unaffected.

If anything the parentheses around the patterns seem to be “grouping patterns” that have to succeed together while single assignment is in effect for all bindings inside the grouping (though names already bound outside of the grouping seem to get a single re-assignment inside the grouping).

2 Likes

Oooo, nice find on the work-around for fixing Elixir’s inconsistency with erlang matchers here!

1 Like