Why use Map.put for structs?

maps
structs

#1

Background

I am reading some code from a book and I noticed the author sometimes uses Map.put and other times he uses the struct update syntax.

Code

Following is an example of such code:

  defstruct state:    :initialized,
            player1:  :islands_not_set,
            player2:  :islands_not_set

  def check(%Rules{state: :players_set} = rules, {:set_islands, player}) do
    rules = Map.put(rules, player, :islands_set)
    case both_players_islands_set?(rules) do
      true  ->  {:ok, %Rules{rules | state: :palyer1_turn}}
      false ->  {:ok, rules}
    end
  end

To me, this is rather confusing. Wouldn’t the previous code be more consistent like this:

  defstruct state:    :initialized,
            player1:  :islands_not_set,
            player2:  :islands_not_set

  def check(%Rules{state: :players_set} = rules, {:set_islands, player}) do
    case both_players_islands_set?(rules) do
      true  ->  {:ok, %Rules{rules | state: :palyer1_turn}}
      false ->  {:ok, %Rules{rules | player1: :islands_set}}
    end
  end

??

What am I missing, are there some performance benefits I am not aware of?
Why would someone choose to code in this style?


PS: I am aware that structs are in fact maps, this question is not about that :smile:


#2

I do not know the full code, but it seems as if the value of player is used to determine which player is to update, your alternate code does seem to update only player1 ever.

An alternative code that would use update syntax had to look more like this:

def check(%Rules{state: :players_set} = rules, {:set_islands, player}) do
  rules = case player do
    :player1 -> %{rules | player1: :islands_set}
    :player2 -> %{rules | player2: :islands_set}
  end

  case both_players_islands_set?(rules) do
    true  ->  {:ok, %Rules{rules | state: :palyer1_turn}}
    false ->  {:ok, rules}
  end
end

#3

A typo in my question, well seen!

So, the author’s way is smaller, which is why he picked. Makes sense!


#4

To be honest, the authors way of using Map.put/3 is also less safe. It might invalidate the struct:

iex(1)> defmodule S do
...(1)>   defstruct [:foo]
...(1)> end
iex(2)> s = %S{}
%S{foo: nil}
iex(3)> s = %{s | foo: :bar}
%S{foo: :bar}
iex(4)> s = Map.put(s, :bar, :foo)
%{__struct__: S, bar: :foo, foo: :bar}
iex(5)> s2 = %S{}
%S{foo: nil}
iex(6)> s2 = %{s2 | bar: :foo}
** (KeyError) key :bar not found in: %S{foo: nil}
    (stdlib) :maps.update(:bar, :foo, %S{foo: nil})
    (stdlib) erl_eval.erl:259: anonymous fn/2 in :erl_eval.expr/5
    (stdlib) lists.erl:1263: :lists.foldl/3

Therefore I consider Map.replace!/3 a better replacement for the update syntax, as it gives us the dynamic approach we need while still guaranteeing to not invalidate the struct.


#5

The best solution is to use Kernel.struct!/2.


#6

Just curious, which book is this from? I’m always interested in books that use games to teach.


#7

The book is Functional Web Development with Elixir, OTP, and Phoenix.


#8

Your code example doesn’t need Map.put, and as you suggested could use the %Rules{rules | foo: :bar} syntax.

The main difference between Map.put and the shortened syntax is that the latter can only update keys which already exist in the map.

map = %{a: 1}

Map.put(map, :a, 2) # => %{a: 2}
%{map | a: 2}         # => %{a: 2}

Map.put(map, :b, 2) # => %{a: 1, b: 2}
%{map | b: 2}         # => DOES NOT COMPILE

Of course when your map is actually a struct, the empty struct will have all of its keys set to nil, so you can do:

defmodule Struct do
  defstruct :a, :b   
end

s = %Struct{}   # => %Struct{a: nil, b: nil}
%{s | a: 1}     # => %Struct{a: 1, b: nil}
%{s | foo: 1}   # => DOES NOT COMPILE

#9

It will actually compile, but fail at runtime. As at compiletime elixir doesn’t know that s has not a key :foo.

As I showed already, it can’t use the map update syntax without a lot of additional repition, as the key is from a variable but the update syntax requires you to know the key at compiletime.