Distilling Maps in Elixir

Gnjulaka
oVice
Published in
7 min readJun 6, 2022

Our team at oVice has been putting a lot of effort into learning Elixir. When learning a new programming language, one must come to an understanding of how that language utilizes data structures. We feel comfortable with how to use the tools Elixir has provided. However, we felt the need to go deeper and understand the why as well. For instance, why use user[:name] instead of user.name? Let’s dig in!

Maps

“Maps are the ‘go to’ key-value data structure in Elixir.”

If you’ve been using Elixir, I’m sure you’ve utilized Maps in one way or the other. While their implementations internally are different, they act much like Ruby’s hash or Python’s dictionary data structures. We’ll showcase some simple functions that Elixir has provided for us. Additionally, when using these tools, it’s important to consider Elixir’s treatment of a map when a key exists vs when it doesn’t.

Adding Keys

Here are two easy ways of adding a key to a map.

Map.put(map, key, value) — Puts the given value under key in map.

iex> user = %{name: “Alice”}
%{name: “Alice”}
iex> Map.put(user, :age, 27)
%{age: 27, name: “Alice”}

Map.put_new(map, key, value) — Puts the given value under key unless the entry key already exists in map.

iex> user = %{name: “Alice”}
%{name: “Alice”}
iex> user = Map.put_new(user, :age, 27)
%{age: 27, name: “Alice”}
iex> user = Map.put_new(user, :age, 28)
%{age: 27, name: “Alice”}

user’s :age remains 27 as that key already exists

However, notice the difference between the two funcitons below:

iex> user = Map.put_new(user, :age, 27)
%{age: 27, name: “Alice”}
iex> user = Map.put(user, :age, 28)
%{age: 28, name: “Alice”}

Map.put provides a map with the new age while Map.put_new does not!

Updating Keys

Updating a map is also very easy!

Map.update(map, key, default, fun) — If the key is not present in a map, default is inserted as the value of key. The default value will not be passed through the update function. This allows for updates of maps without the necessary key.

iex> user = %{name: “Alice”, age: 27}
%{age: 27, name: “Alice”}
iex> Map.update(user, :age, 5, fn current_age -> current_age + 1 end)
%{age: 28, name: “Alice”}

Because the age key exists for Alice, it’s passed to the given function and updates the key’s value.

iex> user = %{name: “John”}
%{name: “John”}
iex> Map.update(user, :age, 5, fn current_age -> current_age + 1 end)
%{age: 5, name: “John”}

As shown, because the age key doesn’t exist on John, the default value was not passed to the given function and John’s age becomes the default value.

Map.update!(map, key, fun) — If the key is not present in a map, a KeyError exception is raised. This is more useful when you want to ensure a key exists before updating.

iex> user = %{age: 27, name: “Alice”}
%{age: 27, name: “Alice”}
iex> Map.update!(user, :age, fn current_age -> current_age + 1 end)
%{age: 28, name: “Alice”}

However, if the key doesn’t exist:

iex> user = %{name: “Alice”}
%{name: “Alice”}
iex> Map.update!(user, :age, fn current_age -> current_age + 1 end)
** (KeyError) key :age not found in: %{name: “Alice”}

And a final syntactic sugar function:

%{map | key_to_update: value}

iex> user = %{age: 27, name: “Alice”}
%{age: 27, name: “Alice”}
iex> user = %{user | age: 28}
%{age: 28, name: “Alice”}

Note, that this function is for updating only and not adding a key. Trying to add a key will result in an error:

iex> user = %{user | occupation: “City Worker”}
** (KeyError) key :occupation not found in: %{age: 27, name: “Alice”}

And it does not play well with the pipe operator (|>)

iex> user |> %{name: “Alice”} ** (ArgumentError) cannot pipe user into %{name: “Alice”}, can only pipe into local calls foo(), remote calls Foo.bar() or anonymous function calls foo.()

Mixing and Matching Keys

Another interesting aspect of Elixir is the atom type. This basic type is different from a string, but both can be used as keys within a map. In fact, anything can be used as a key for a map. However, the syntax is different if atoms are used.

The : or => syntax can be used for atoms

iex> user = %{age: 27, name: “Alice”}
%{age: 27, name: “Alice”}

or

iex> user = %{:age => 27, :name => “Alice”}
%{age: 27, name: “Alice”}

However, for everything else, only => can be used

Strings

iex> %{“coolkey” => “String!”}
%{“coolkey” => “String!”}

Tuples

iex> %{{:ok, :success} => “Tuple!”}
%{{:ok, :success} => “Tuple!”}

Functions

iex>%{&is_atom/1 => “Atom Function”}
%{&:erlang.is_atom/1 => “Atom Function”}

and if mixing and matching types, usage of syntax must be specific

iex> user = %{:name => Alice, “age”=> 27}
%{:name => Alice, “age” => 27}

Using : and => together, : must come last in the map definition

iex> user = %{name: Alice, “age”=> 27}
** (SyntaxError) iex:50:20: unexpected expression after keyword list. Keyword lists must always come last in lists and maps.
iex> user = %{“age” => 27, name: “Alice”}
%{:name => “Alice”, “age” => 27}

Strict Vs. Dynamic Access

Elixir provides two syntaxes for accessing values. These are referred to as “Dynamic” and “Static” lookups. For Maps, typically, the key-values are not set in stone and can change. Due to this attribute, Maps are considered “dynamic structures.” As such, both “Dynamic” and “Static” lookups are provided for Maps up front.

Say we have the following map

iex> user = %{name: “Alice”, age: 27}
%{name: “Alice”, age: 27}

We can access it using Strict Lookup

iex> user.name
“Alice”

or Dynamic Lookup

iex> user[:name]
“Alice”

Both of these methods have pros and cons. Strict lookup will raise an error if that key does not exist, while Dynamic lookup will return nil.

Strict Lookup

iex> user.occupation
** (KeyError) key :occupation not found in: %{:name => Alice, “age” => 27}

Dynamic Lookup

iex> user[:occupation]
Nil

José, from his blog post at https://dashbit.co/blog/writing-assertive-code-with-elixir, recommends to use strict when possible, but their usage is up to you! Say you want to fail fast to catch errors early in development and make debugging easier? Or, maybe you’re dealing with a database model that has its fields set and won’t change? Strict access might be better suited for your situation! However, say you just want to quickly iterate over a map where the key-values are unknown? What if you want to verify a key exists within a map without returning an error? Dynamic access may be just what you need. This discussion of access segues into another implementation of a Map.

Structs

“Structs are extensions built on top of maps that provide compile-time checks and default values.Or, more importantly: “structs are bare maps with a fixed set of fields.”

Strict Vs. Dynamic Access Continued

As was mentioned, Structs are bare maps. What does this mean? To quote the documentation: “Notice that we referred to structs as bare maps because none of the protocols implemented for maps are available for structs.” Therefore, you cannot enumerate or access a struct dynamically. To begin, you have to define a module with a corresponding struct

iex> defmodule User do
…> defstruct name: “Alice”, age: 27
…> end
{:module, User, <<70, 79, 82, 49, 0, 0, 6, 200, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 193, 0, 0, 0, 19, 11, 69, 108, 105, 120, 105, 114, 46, 85, 115, 101, 114, 8, 95, 95, 105, 110, 102, 111, 95, 95, 10, 97, …>>, %User{age: 27, name: “Alice”}}

then you can create your struct

iex> user = %User{}
%User{age: 27, name: “Alice”}

Keep this in mind as you cannot create a struct if it has not been defined.

iex> email = %Email{}
** (CompileError) iex:10: Email.__struct__/1 is undefined, cannot expand struct Email. Make sure the struct name is correct. If the struct name exists and is correct but it still cannot be found, you likely have cyclic module usage in your code

That leads us into actually utilizing the struct itself. As stated, we can’t dynamically access a struct but we can strictly access them. This is due to the Elixir design decision that structs are “static structures” where their fields do not change.

Strict Lookup

iex> user.age
27

Dynamic Lookup

iex> user[:age]
** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour) User.fetch(%User{age: 27, name: “Alice”}, :age) (elixir 1.13.2) lib/access.ex:285: Access.get/3

Additionally, we cannot enumerate over a struct

iex> Enum.each(user, fn {key, _value} -> IO.puts(key) end)** (Protocol.UndefinedError) protocol Enumerable not implemented for %User{age: 27, name: “Alice”} of type User (a struct)
(elixir 1.13.2) lib/enum.ex:1: Enumerable.impl_for!/1
(elixir 1.13.2) lib/enum.ex:143: Enumerable.reduce/3
(elixir 1.13.2) lib/enum.ex:4144: Enum.each/2

Map Module

Although structs are bare maps, you can use the Map module for some additional functionality.

iex> user = Map.put(user, :occupation, “City Worker”)
%{__struct__: User, age: 27, name: “Alice”, occupation: “City Worker”}
iex> Map.keys(user)
[:__struct__, :age, :name, :occupation]

You can also convert a struct to a map and get all of the standard functionality back using the Map.from_struct function

iex> user = Map.from_struct(user)
%{age: 27, name: “Alice”, occupation: “City Worker”}
iex> user[:name]
“Alice”

Summary

Maps are very versatile and have a lot of functionality baked in. When in doubt, take a look at https://hexdocs.pm/elixir/1.12/Map.html#summary and I’m sure you’ll find a function that meets your needs!

Sources:

https://dashbit.co/blog/writing-assertive-code-with-elixir

https://hexdocs.pm/elixir/1.5.0-rc.1/Access.html

https://hexdocs.pm/elixir/1.12/Map.html

https://hexdocs.pm/elixir/1.13/Kernel.html#struct/2

https://elixir-lang.org/getting-started/keywords-and-maps.html

https://elixir-lang.org/getting-started/structs.html

https://alchemist.camp/articles/how-to-update-elixir-maps

--

--