PureType

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) do
2 nil ->
3 # :key doesn't exist in the map
4
5 value ->
6 # do something with value
7end

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) do
2 {:ok, value} ->
3 # do something with value
4
5 :error ->
6 # key doesn't exist in the map
7end

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# or
3map.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) do
2 :error -> DEFAULT
3 {:ok, value} -> value
4end

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 return nil as the default value

  • Map.get(map, :key, DEFAULT) - if you want to return an (inexpensive) default value

  • Map.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.