Map lookup in Elixir - a taxonomy
There are several ways to look up values in a map in Elixir (%{a: “b”}
), given a single key or multiple keys. This article will make clear which ones are more suitable in which scenario, along with an anti-pattern sometimes seen in code.
Keys are most typically atoms or strings, though the language does not restrict this. Anything can be a key in a map!
If you want to pick up idiomatic patterns in the language quickly by use case, this is the article for you.
Related Elixir concepts
The Access behaviour allows for a common syntax ([]
) to look up keys in data structures in Elixir. You’ll see it mostly with the a[:b]
syntax, though it helps with looking up values in nested structures too (get_in is a good example of how Access helps)
Structs extend from maps and provide compile-time checks and default values for members. (%User{name: “test”}
is a struct, %{name: “test”}
is not).
Structs do not inherit the Access
behaviour, so you can’t look up keys in structs using the []
synax. It’ll be clear below when behaviour actually differs between the two types.
Single key
Branch if the key doesn’t exist
Sometimes you’ll see Map.get
or []
used for this purpose. Something like the below:
1case Map.get(map, :key) do2 nil ->3 # :key doesn't exist in the map4 5 value ->6 # do something with value7end
but this is a risky route to tread, especially if nil
is a possible (or future possible!) valid value.
Map.fetch
is the best way to express this. It avoids the problem of using nil
as a sentinel value:
1case Map.fetch(map, :key) do2 {:ok, value} ->3 # do something with value4 5 :error ->6 # key doesn't exist in the map7end
Error if the key doesn’t exist
.key
works if your map uses exclusively atoms for keys. Map.fetch!
works well for strings and for the general case, since keys can be of any type in Elixir:
1Map.fetch!(map, :key)2# or3map.key
Both throw KeyError
if there is no entry with :key
in map
. But if you need to handle that case elegantly, you would use Map.get
.
Again, avoid the mistake of using Map.get
or []
- it also encodes a clear message to the reader about the necessity of :key
already being present in map
- it makes it much easier to reason about the remainder of the function as a result.
Default value if the key doesn’t exist
Often you will see code using case
and separate cases for present and missing values to provide a default: e.g. using the :error
return value from Map.fetch
or nil
from Map.get
- something like:
1case Map.fetch(map, :key) do2 :error -> DEFAULT3 {:ok, value} -> value4end
That’s more typing than necessary!
You’ll want to return a default value if a given key is not present. You have three options:
map[:key]
- if you wish to returnnil
as the default valueMap.get(map, :key, DEFAULT)
- if you want to return an (inexpensive) default valueMap.get_lazy(map, :key, fn -> DEFAULT end)
- if you want to return a default value that is expensive to calculate. The third paramater is a function that is only called when:key
is not present.
These methods don’t apply to structs, since these keys must always exist in the map and are initialised with a default value when the struct instance is created.
Multiple keys
Throw if any desired keys do not exist
Instead of using Map.fetch!
or .key
several times in a row, use the power of Elixir’s pattern matching:
1%{key: v, other_key: ov} = map
It will throw MatchError
if either (or both) key
and another_key
are missing.
Ignore if any desired keys do not exist
Map.take
creates a map containing the given keys. If they don’t exist in the given map, they are ignored:
1Map.take(%{a: 1, b: 2, c: 3}, [:a, :c, :e])
returns: %{a: 1, c: 3}
Retrieve keys with defaults
Instead of using multiple Map.get
(with default) statements in a row, using Map.merge
you can compress into a single readable line:
1%{key: v, other_key: ov} =2 Map.merge(%{key: 1, other_key: 2}, map)
Conclusion
There are a variety of ways to look up values by key in maps. Each way serves a slightly different purpose, but using the right method gives more context to the reader and usually results in less code.
Each scenario above is relevant for retrieving the value stored by a key. However, sometimes you’ll want to just check for a key’s existence in a map. Map.has_key? is perfect for this purpose. Using the functions above to check this, and discarding the retrieved value, would be confusing to follow.
Would you like to see your code evolve to incorporate these conventions? PureType will spot less-than-ideal usage, and many other style and form issues, following up with relevant exercises personalised to your work.