5

In the book 'Elixir in Action', one of the examples has a function that is tripping up my understanding of pattern matching:

def add_entry(
    %TodoList{entries: entries, auto_id: auto_id} = todo_list,
    entry
  ) do
    entry = Map.put(entry, :id, auto_id)
    new_entries = HashDict.put(entries, auto_id, entry)

    %TodoList{todo_list |
      entries: new_entries,
      auto_id: auto_id + 1
    }
 end

The first parameter, %TodoList{entries: entries, auto_id: auto_id} = todo_list, the book explains "...Furthermore, you keep the entire instance in a todo_list variable"

This confuses me because I thought variables get bound on the left side of a '=' pattern matching operator. Could someone help explain what is happening with the first parameter and how the incoming values are able to be used inside the function body?

Adam Millerchip
  • 20,844
  • 5
  • 51
  • 74
Thomas
  • 465
  • 1
  • 4
  • 14

3 Answers3

4

I thought variables get bound on the left side of a '=' pattern matching operator

That's correct, in this case the entries and auto_id variables are bound. The right-hand side todo_list is bound from the argument to the function.

It's like doing this:

iex(1)> foobar = %{foo: "foo", bar: "bar"}
%{bar: "bar", foo: "foo"}
iex(2)> %{foo: matched} = foobar
%{bar: "bar", foo: "foo"}
iex(3)> matched
"foo"

The only difference when doing it in a function signature is that the first step of defining what becomes the right-hand-side is handled automatically.

As the book says, you can define a function signature like this:

def do_something_with_foo(%{foo: matched} = original)

Which is explained above, where both matched and original are available in the function body. If you only care about the matched values, you can omit the right-hand side:

def do_something_with_foo(%{foo: matched})

In this case, only the matched value matched will be available. The match still happens, but the data structure passed as the first argument to the function that is used implicitly as the right-hand side, as if you had used =, is not bound to a variable.

Adam Millerchip
  • 20,844
  • 5
  • 51
  • 74
  • 1
    It’s probably worth to mention that also one might `def do_smth(matched = %{foo: original})` where the real magic actually happens :) Try `def f(%{al: al} = a = %{ar: ar}, do: IO.inspect({al, a, ar})` called as `f(%{al: :left, ar: :right})`. If the assignment was done _right to left_, we’d get `MatchError` there, but `a` is totally fine. – Aleksei Matiushkin Jun 26 '19 at 05:21
  • Also: `%{a_left: a_left} = a_full = %{} = a_full = %{a_right: a_right} = %{a_left: 42, a_right: 42}`. Looks like in the call to the function, [tag:erlang] assigns an implicit `_arg` and then matches everything in the chain against it. – Aleksei Matiushkin Jun 26 '19 at 05:34
4

With variables:

iex(2)> x = %{a: 1, b: 2}   
%{a: 1, b: 2}

iex(3)>  %{a: 1, b: 2} = y
** (CompileError) iex:3: undefined function y/0

With function parameter variables:

defmodule A do

  def go1(z = %{a: a}) do
    IO.inspect z
    IO.puts a
  end

  def go2(%{a: a} = z) do
    IO.inspect z
    IO.puts a
  end

end

In iex:

iex(4)> c "a.ex"  
warning: redefining module A (current version defined in memory)
  a.ex:1
[A] 

iex(5)> map = %{a: 1, b: 2} 
%{a: 1, b: 2}

iex(6)> A.go1(map)
%{a: 1, b: 2}
1
:ok

iex(7)> A.go2(map)
%{a: 1, b: 2}
1
:ok

Function args are pattern matched to the function parameter variables. And, in elixir function parameters can be constants, e.g. 1 or an atom, maps, tuples, etc--not just a variable, like x, y, or z. Here is how go1() works:

    A.go1(%{a: 1 b: 2})
          |-----+----|
                |
                | %{a: a} = %{a: 1 b: 2} 
                V
def go1(z = %{a: a}) do

The "parameter variable" is %{a: a}, and it gets matched to the function argument %{a: 1 b: 2}, which binds a to 1. Then, you might think that you get the pattern match: z = %{a: 1}, however, the pattern match %{a: a} = %{a: 1 b: 2} actually returns the right hand side:

iex(10)> %{a: a} = %{a: 1, b: 2}    
%{a: 1, b: 2}

Therefore, you get the pattern match: z = %{a: 1, b: 2}. Here is another demonstration of that:

iex(13)> z = %{a: a} = %{a: 1, b: 2}
%{a: 1, b: 2}

iex(14)> a
1

iex(15)> z
%{a: 1, b: 2}

Here is how go2() works:

      A.go1(%{a: 1 b: 2})
            |-----+----|
                  |
                  | z = %{a: 1, b: 2}
                  V
def go2(%{a: a} = z)

z is the parameter variable and it gets matched to the function argument %{a: 1 b: 2}. The match z = %{a: 1 b: 2} returns the right hand side:

iex(10)> z = %{a: 1, b: 2}
%{a: 1, b: 2}

So, next you get the pattern match: %{a: a} = %{a: 1, b: 2}, which binds a to 1.

Therefore, all is consistent: with every pattern match, the pattern containing the variables is on the left of =, and the values are on the right side. If you are looking for a rule it's: in the parameter list for a function definition, the thing on the right of an = sign is the "parameter variable". The thing on the left is a pattern that will get matched after the "parameter variable" is matched to the function argument (or as you would say in other languages: "after the function argument is assigned to the parameter variable").

7stud
  • 46,922
  • 14
  • 101
  • 127
  • 1
    The former example (`A.go1(%{a: 1 b: 2})`) is not very accurate, though. If _“Then you get the pattern match: `z = %{a: 1}`.”_ was a thing, `z` would have been bound to `%{a: 1}`, while it gets bound to the whole argument in the call. See my comments to another answer for clarification. – Aleksei Matiushkin Jun 26 '19 at 05:30
  • Good catch! Fixed. – 7stud Jun 26 '19 at 17:44
0

When you specify %{map_thing: stuff} = map_var you're saying I expect that this var will contain the variable map_thing, and I want you to put the entire contents of this map into the variable map_var. You can use this to explicitly specify required keys in that map, and also grab all the optional ones and "bind" them to your map_var

You can do all sorts of useful stuff with this like creating a sort of psuedo guard in case

case some_var do
%MyStruct{} = struct_var ->
 # we are saying we expect this var to be of the `MyStruct` variety.
other_case ->
  do_something_else()
end
Ian
  • 544
  • 3
  • 16