Elixir Best Practices - Deeply Nested Maps

nested
Brian Cardarella

CEO & Founder

Brian Cardarella

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.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box