Let's continue the second half of our in-depth look at the Map module functions. As with most of our in-depth coverage of Elixir modules so far, I've found the Map module functions to be more interesting than I had anticipated.

Map.pop/3

The Map.pop/3 function is very similar to Map.get/3, except that it returns both the value that matches the key and another map that is the original map minus the key. This is the Elixir equivalent to removing the key-value entry from the map and returning the value.

Since all data in Elixir is immutable, it can't just remove the key from the original map. Instead, it returns a transformed map that results in the same thing. Of course, this far more efficient to do than it would be in most languages because immutability allows both maps to share most of their data under the surface, making it possible to create a modified map with very little copying.

The value that was popped from the map is returned as the first element in a tuple and the modified map is returned as the second element in a tuple. If the key does not exist, the default value is returned for the value and the original map is returned in place of a modified map.

The third parameter is an optional parameter that allows you to specify the default value. By default (heh, heh), the default value is nil.

iex> person = %{name: "Mao", age: 54}
%{age: 54, name: "Mao"}
iex> Map.pop(person, :name)
{"Mao", %{age: 54}}
iex> Map.pop(person, :age)
{54, %{name: "Mao"}}
iex> Map.pop(person, :birthdate)
{nil, %{age: 54, name: "Mao"}}
iex> Map.pop(person, :birthdate, :not_found)
{:not_found, %{age: 54, name: "Mao"}}

Map.pop_lazy/3

The Map.pop_lazy/3 function is the pop equivalent of Map.get_lazy/3. It pops a value from a map just like Map.pop/3, but it also receives a function that generates the default value when the key is missing. Use this function if calculating the default value is expensive.

iex> person = %{name: "Mao", age: 54}
%{age: 54, name: "Mao"}
iex> Map.pop_lazy(person, :name, fn -> :not_found end)
{"Mao", %{age: 54}}
iex> Map.pop_lazy(person, :birthdate, fn -> :not_found end)
{:not_found, %{age: 54, name: "Mao"}}

Map.put/3

The Map.put/3 function sets the given key in a map to a given value. If that key already exists, its value is overwritten. If the key does not exist, it's added. The function returns the modified map.

iex> person = %{name: "Mao", age: 54}
%{age: 54, name: "Mao"}
iex> Map.put(person, :name, "Bob")
%{age: 54, name: "Bob"}
iex> Map.put(person, :birthdate, ~D(1950-04-03))
%{age: 54, birthdate: ~D[1950-04-03], name: "Mao"}

Map.put_new/3

The Map.put_new/3 function works similarly to Map.put/3 except that it will never update an existing key. The map will only be modified if the key is new. If the given key already exists in the map, the original unmodified map is returned.

iex> person = %{name: "Mao", age: 54}
%{age: 54, name: "Mao"}
iex> Map.put_new(person, :name, "Bob")
%{age: 54, name: "Mao"}
iex> Map.put_new(person, :birthdate, ~D(1950-04-03))
%{age: 54, birthdate: ~D[1950-04-03], name: "Mao"}

Map.put_new_lazy/3

The Map.put_new_lazy/3 function works like Map.put_new/3, except that it lazily computes the value, similar to Map.get_lazy/3 and Map.pop_lazy/3. You pass a function that generates the value. That function is only called when the key is not already present in the map.

This function is useful when the value being added to the map is expensive to computer and you only want to calculate/retrieve the value when it is truly needed.

iex> person = %{name: "Mao", age: 54}
%{age: 54, name: "Mao"}
iex> Map.put_new_lazy(person, :name, fn -> "Lazy Name" end)
%{age: 54, name: "Mao"}
iex> Map.put_new_lazy(person, :birthdate, fn -> ~D(1950-04-03) end)
%{age: 54, birthdate: ~D[1950-04-03], name: "Mao"}

Map.replace!/3

The Map.replace!/3 function is the opposite of Map.put_new/3. It will only update existing keys and will never add new ones. As can be seen from the function name, this function throws an exception (a KeyError) when the given key is not present in the map.

iex> person = %{name: "Mao", age: 54}
%{age: 54, name: "Mao"}
iex> Map.replace!(person, :name, "Bob")
%{age: 54, name: "Bob"}

iex> Map.replace!(person, :birthdate, ~D(1950-04-03))
** (KeyError) key :birthdate not found in: %{age: 54, name: "Mao"}
    (stdlib) :maps.update(:birthdate, ~D[1950-04-03], %{age: 54, name: "Mao"})

In testing out this function, I accidentally called Map.replace/3 instead, forgetting the exclamation mark (!). It turned out that the Map module has this function as well, but it's marked as deprecated. That's certainly why it doesn't appear in the documentation. Documentation must not be generated for deprecated functions.

iex> Map.replace(person, :name, "Bob")
warning: Map.replace/3 is deprecated. Use Map.fetch/2 + Map.put/3 instead
  iex:19
  
%{age: 54, name: "Bob"}

iex> Map.replace(person, :birthdate, ~D(1950-04-03))
warning: Map.replace/3 is deprecated. Use Map.fetch/2 + Map.put/3 instead
  iex:21

%{age: 54, name: "Mao"}

That hidden, deprecated function works just like how I would expect a non-exception-throwing version of Map.replace!/3 to work. It's interesting that it tells us to use a combination of Map.fetch/2 and Map.put/3 instead. Let's try that out.

iex> person = %{name: "Mao", age: 54}
%{age: 54, name: "Mao"}
iex> Map.fetch(person, :birthdate)
:error
iex> Map.fetch(person, :name)
{:ok, "Mao"}
iex> case Map.fetch(person, :birthdate) do
...> {:ok, _} -> Map.put(person, :birthdate, ~D(1950-04-03))
...> :error -> person
...> end
%{age: 54, name: "Mao"}
iex> case Map.fetch(person, :name) do
...> {:ok, _} -> Map.put(person, :name, "Bob")
...> :error -> person
...> end
%{age: 54, name: "Bob"}

Map.fetch/2 returns a tuple containing :ok when the key is there and :error when the key is not there. So I can use a case expression to duplicate the functionality of the deprecated Map.replace/3. I wonder why they deprecated that function: it looks useful.

Out of curiosity, I went to the source code for the Map module on Github to look at what the deprecated Map.replace/3 does.

  @doc false
  @deprecated "Use Map.fetch/2 + Map.put/3 instead"
  def replace(map, key, value) do
    case map do
      %{^key => _value} ->
        put(map, key, value)

      %{} ->
        map

      other ->
        :erlang.error({:badmap, other})
    end
end

It does pretty much the same thing as what I did. I imagine that the @doc false directive is what causes it to be left out of the documentation. The @deprecated directive clearly marks it as deprecated. I was surprised at how simple the source code for the Map module is. The functions are quite small and simple and the documentation is far larger than the code it is documenting.

That was a bit of a digression, but it's so much fun when I stumble across stuff like this! Exploring helps me to better learn Elixir.

Map.take/2

The Map.take/2 function works just like Map.split/2 except that it only returns one map: the map containing the matching keys. The map returned will contain only the keys that were present in the map being passed in and in the list of keys.

iex> person = %{name: "Mao", age: 54}
%{age: 54, name: "Mao"}
iex> Map.take(person, [:name])
%{name: "Mao"}
iex> Map.take(person, [:name, :age])
%{age: 54, name: "Mao"}
iex> Map.take(person, [:name, :birthdate])
%{name: "Mao"}
iex> Map.take(person, [])
%{}

Map.to_list/1

The Map.to_list/1 function converts a map into a list of tuples, where each tuple contains a key-value pair. You can think of this as the opposite of Map.new/1.

iex> person = %{name: "Mao", age: 54, birthdate: ~D(1950-04-03)}
%{age: 54, birthdate: ~D[1950-04-03], name: "Mao"}
iex> Map.to_list(person)
[age: 54, birthdate: ~D[1950-04-03], name: "Mao"]
iex> Map.to_list(%{2 => "two", 3 => "three"})
[{2, "two"}, {3, "three"}]
iex> Map.to_list(%{})
[]

The first example, where the map keys are all atoms, displays the resulting list as a keyword list, which is just a list of 2-element tuples. The second example uses non-atom keys, so you can more clearly see the tuples, since the list is no longer displayed using keyword list syntax.

Map.update!/3

The Map.update!/3 function works similarly to Map.replace!/3 except that it uses a function to update the existing key with a new value. That function receives the current value as an input parameter and returns the new value. If the key is not found, a KeyError is thrown.

iex> person = %{name: "Mao", age: 54}
%{age: 54, name: "Mao"}
iex> Map.update!(person, :name, fn name -> name <> " " <> name end)
%{age: 54, name: "Mao Mao"}
iex> Map.update!(person, :age, &(&1 + 1))
%{age: 55, name: "Mao"}
iex> Map.update!(person, :birthdate, &(&1 + 1))
** (KeyError) key :birthdate not found in: %{age: 54, name: "Mao"}
    (stdlib) :maps.get(:birthdate, %{age: 54, name: "Mao"})
    (elixir) lib/map.ex:267: Map.update!/3

Map.update/4

The Map.update!/4 function is similar to the Map.update!/3 function except that it will add a key and a value if the key doesn't already exist.

The second parameter is the key to be updated, the third parameter is the value to be added when the key doesn't exist, and the fourth parameter is the function that determines the value when the key does exist. The function allows an update to happen using a function for existing keys and a default value to be added for non-existent keys.

iex> person = %{name: "Mao", age: 54}
%{age: 54, name: "Mao"}
iex> Map.update(person, :name, "Bob", &(&1 <> " " <> &1))
%{age: 54, name: "Mao Mao"}
iex> Map.update(person, :nickname, "Bob", &(&1 <> " " <> &1))
%{age: 54, name: "Mao", nickname: "Bob"}

Map.get_and_update!/3

The Map.get_and_update!/3 function behaves the same way as Map.update!/3, except that it returns the old value prior to the update. It returns a tuple where the first element is the old value and the second element is the map after it was updated. This is useful when you want to retrieve the old value and update it in a single step.

The function that does the updating also needs to return a tuple where the first element is the current element to be returned and the second element is the value to use in updating. I guess this allows you to change the value that is returned.

iex> person = %{name: "Mao", age: 54}
%{age: 54, name: "Mao"}
iex> Map.get_and_update!(person, :name, fn name -> {name, name <> " " <> name} end)
{"Mao", %{age: 54, name: "Mao Mao"}}
iex> Map.get_and_update!(person, :name, fn name -> {"Bob", name <> " " <> name} end)
{"Bob", %{age: 54, name: "Mao Mao"}}
iex> Map.get_and_update!(person, :nickname, fn name -> {name, name <> " " <> name} end)
** (KeyError) key :nickname not found in: %{age: 54, name: "Mao"}
    (stdlib) :maps.get(:nickname, %{age: 54, name: "Mao"})
    (elixir) lib/map.ex:267: Map.get_and_update!/3

Map.get_and_update/3

The Map.get_and_update/3 function is similar to Map.get_and_update/3, except there are two differences. Firstly, if a key doesn't exist, it will be added, and the update function will be called to create a value for that new key. Secondly the update function has the option to return :pop, in which case the key is actually removed from the map and the value is returned, which is how Map.pop/3 behaves. I guess this behavior allows you to either update or delete a key depending on what its value is, which can be done in a single function call instead of multiple.

This function is quite complicated, and I suspect that this is one of those more generic functions that can be used to implement any of the other add, update, and delete functions. That means that you should be able to use this function to implement add, update, and delete functions with your own customized logic.

iex> person = %{name: "Mao", age: 54}
%{age: 54, name: "Mao"}
#Example of adding the key when it doesn't exist
iex> Map.get_and_update(person, :nickname, fn
...> nil -> {nil, "Bobby"}
...> name -> {name, name <> " " <> name}
...> end)
{nil, %{age: 54, name: "Mao", nickname: "Bobby"}}
#Example of updating an existing key
iex> Map.get_and_update(person, :name, fn
...> nil -> {nil, "Bobby"}
...> name -> {name, name <> " " <> name}
...> end)
{"Mao", %{age: 54, name: "Mao Mao"}}
#Example of deleting a key
iex> Map.get_and_update(person, :nickname, fn _ -> :pop end)
{nil, %{age: 54, name: "Mao"}}
iex> Map.get_and_update(person, :name, fn _ -> :pop end)
{"Mao", %{age: 54}}

Of course it would make the most sense to create a reusable update function that decides when to add, update, and delete so that you don't have to type it out every time you pass it to this function.

Map.values/1

Much like how Map.keys/1 returns a list of all the keys in the map, the Map.values/1 function returns a list of all the values in a map.

iex> person = %{name: "Mao", age: 54, birthdate: ~D(1950-04-03)}
%{age: 54, birthdate: ~D[1950-04-03], name: "Mao"}
iex> Map.values(person)
[54, ~D[1950-04-03], "Mao"]
iex> Map.values(%{name: "Bob"})
["Bob"]
iex> Map.values(%{})
[]

Those are all the functions in the Map module. We certainly have a lot of options for working with maps.