When writing Elixir apps you’ll typically find yourself building up state in a map. Typically these maps contain deep nesting. Updating anything deeply nested means you have to write something like:
my_map = %{
foo: %{
bar: %{
baz: "my value"
}
}
}
new_bar_map =
my_map
|> Map.get(:foo)
|> Map.get(:bar)
|> Map.put(:baz, "new value")
new_foo_map =
my_map
|> Map.get(:foo)
|> Map.put(:bar, new_bar_map)
Map.put(my_map, :foo, new_foo_map)
That’s pretty complex for a simple nested key update!
Elixir has a better way: Kernel.put_in/3
This function uses the Access “behaviour” to drastically reduce the keystrokes for inserting into deeply nested maps. Let’s take a look at refactoring the above example:
my_map = %{
foo: %{
bar: %{
baz: "my value"
}
}
}
put_in(my_map, [:foo, :bar, :baz], "new value")
That’s it! The really nice thing about this function is that the 2nd argument is simply a list which means when we’re dealing building complex maps during recursion we can simply append to the list.
Similarly, we can get deeply nested values in a map using
Kernel.get_in/2
:
my_map = %{
foo: %{
bar: %{
baz: "my value"
}
}
}
get_in(my_map, [:foo, :bar, :baz]) == "my value"
Let’s go deeper. Does the list syntax feel like too many characters to
you? Let me introduce you to Kernel.put_in/2
put_in(my_map.foo.bar.baz, "new value")
This version of the function is a macro that will break up the syntax and deal with each part of the path individually. It may feel like magic but it’s just Elixir doing what it does best: blowing your mind.
Still going deeper…
Now its time to get fancy. Let’s say you want to update the values
according to a function. To do that we’ll use
Kernel.update_in/3
my_map = %{
bob: %{
age: 36
}
}
update_in(my_map, [:bob, :age], &(&1 + 1))
#=> %{bob: %{age: 37}}
update_in(my_map.bob.age, &(&1 + 1))
#=> %{bob: %{age: 37}}
Dealing with Lists and Structs
Deeply nested lists can also make use of these functions. However, there is a difference in the short-hand syntax.
my_list = [foo: [bar: [baz: "my value"]]]
put_in(my_list[:foo][:bar][:baz], "new value")
This is referred to as “field-based lookup” and can differ depending upon the type you are acting upon. Maps can work with either form:
my_map[:foo][:bar][:baz]
#=> "my value"
my_map.foo.bar.baz
#=> "my value"
Lists only work with the bracket form:
my_list[:foo][:bar][:baz]
#=> "my value"
my_list.foo.bar.baz
#=> ** (ArgumentError) argument error
Structs only work with the path form:
my_struct.foo.bar.baz
#=> "my value"
my_struct[:foo][:bar][:baz]
#=> ** (UndefinedFunctionError) undefined function MyStruct.fetch/2
I hope this helps you deal with deeply nested maps, lists, and structs!
DockYard is a digital product agency offering exceptional user experience, design, full stack engineering, web app development, custom software, Ember, Elixir, and Phoenix services, consulting, and training.