Updating Dynamic Maps in Elixir

Maps are used extensively in Elixir. Updating nested maps in Elixir is straightforward if you already know the structure of your map beforehand (in this case, you may want to use a Struct instead).

But updating dynamic maps in Elixir, especially if they are nested, can be a bit difficult. EDIT: After publishing this post, I realized that I had forgotten about the incredibly helpful Map.update/4 which can make updating maps which have 1 layer of nesting easy.

However, it does not work if your map is nested several layers deep. In which case, the following trick still applies. I have updated the post accordingly.

I’ll show you a little talked about and documented way of updating a dynamic nested map in Elixir.

Elixir Dynamic Maps

Dynamic Nested Maps in Elixir

Let’s say you have a nested map:

inventory = %{
  cameras: %{}
}

Because the “cameras” key already exists in inventory map, updating the “cameras” map is straightforward in Elixir.


get_and_update_in(inventory.cameras, fn(cameras) ->
  {cameras, put_in(cameras, [10_001], "Nikon D90")}
end)
// %{cameras: %{10_001 => "Nikon D90"}}

or 

Map.update(inventory, :cameras, %{10_001 => "Nikon D90"}, &Map.put(&1, 10_001, "Nikon D90"))
// %{cameras: %{10_001: "Nikon D90"}}

But what if we want to add a key that doesn’t exist?

Let’s say we want to add our first lens to our inventory using the :lens key under inventory.

iex(1)> put_in(inventory, [:lenses, 10_002], "Nikon 50mm F1.4")
** (ArgumentError) could not put/update key 10002 on a nil value

Uh-oh. The :lenses key doesn’t exist in inventory yet, so put_in/3 will not allow us to add to it.

Now, there’s one way we could it if we know that lenses doesn’t exist yet. By using Map.merge/2:

Map.merge(inventory, %{lenses: %{10_002: "Nikon 50mm F1.4"}})
// %{
  cameras: %{
    10_001 => "Nikon D90"
  }, 
  lenses: %{
    10_002 => "Nikon 50mm F1.4"
  }
}

However, this solution won’t work if :lenses already exists and there are keys and values present. Let’s say we already have a lens and want to add one to the collection. The merge will actually overwrite the existing lens instead of adding to it.

inventory = %{
  cameras: %{
    10_001 => "Nikon D90"
  }, 
  lenses: %{
    10_002 => "Nikon 50mm F1.4"
  }
}

Map.merge(inventory, %{lenses: %{10_003: "Nikon 85mm F1.8"}})

// %{
  cameras: %{
    10_001 => "Nikon D90"
  }, 
  lenses: %{
    10_003 => "Nikon 85mm F1.8"
  }
}

Uh-oh. Not what we wanted either. OK, well, a prolonged solution would be to check if the lenses key exists in inventory first, then use a condition to either update the lens or create a new map…

Ugh, that’s too much work.

Map.update/4 with Nested Elixir Maps

There’s a convenient way to update a nested map that is 1 layer deep which already exists and that is the Map.update/4 function we saw earlier.

inventory = %{
  cameras: %{
    10_001 => "Nikon D90"
  }, 
  lenses: %{
    10_002 => "Nikon 50mm F1.4"
  }
}

Map.update(inventory, :lenses, %{10_003 => "Nikon 85mm F1.8"}, &Map.put(&1, 10_003, "Nikon 85mm F1.8"))
// %{
  cameras: %{
    10_001: "Nikon D90"
  }, 
  lenses: %{
    10_002: "Nikon 50mm F1.4", 
    10_003: "Nikon 85mm F1.8"
  }
}

The key part of this function is the third argument. It is the default value that will be used if the key in the second argument cannot be found in the inventory map provided.

But, if a key and value are found, the value will be passed to the function in the 4th argument, in which case, we simply add the new lens to the collection using Map.put/3.

However, as I mentioned before, Map.update/4 will not work if you are trying to update a map which is more than one layer deep.

Let’s rework our example to use deeply nested maps. Let’s say our camera inventory is divided up by brand which are represented as maps.

inventory = %{
  cameras: %{
    "Nikon" => %{
      10_001 => "Nikon D90"
    },
    "Canon" => %{}
  }
}

Let’s say we want to add a Canon camera to the inventory.

inventory = %{
  cameras: %{
    "Nikon" => %{
      10_001 => "Nikon D90"
    },
    "Canon" => %{}
  }
}

Map.update(inventory, "Canon", %{10_004 => "Canon 50D"}, &Map.put(&1, 10_004, "Canon 50D"))

// %{
  "Canon" => %{
    10_004 => "Canon 50D"
  },
  cameras: %{
    "Nikon" => %{
      10_001 => "Nikon D90"
    },
    "Canon" => %{}
  }
}

Ugh. Not what we wanted. OK, so how do we update maps which nested more than 1 layer deep?

There’s actually a rarely talked about way to do this.

The Access.key/2 function in Elixir with Deeply Nested Maps

We can actually use a combination of the get_and_update_in/3 and the Access.key/2 function.

This solution will work in the cases where "Canon" map exists and has keys and values present or does not.

inventory = %{
  cameras: %{
    "Nikon" => %{
       10_001 => "Nikon D90"
    }
  }
}

get_and_update_in(inventory, [Access.key(:cameras, %{}), Access.key("Canon", %{})], fn(canons) ->
  {canons, put_in(canons, [10_004], "Canon 50D")}
end)

// %{
  cameras: %{
    "Nikon" => %{
      10_001 => "Nikon D90"
    },
    "Canon" => %{
      10_004 => "Canon 50D"
    }
  }
}
inventory = %{
  cameras: %{
    "Nikon" => %{
       10_001 => "Nikon D90"
    },
    "Canon" => %{
      10_004 => "Canon 50D"
    }
  }
}

get_and_update_in(inventory, [Access.key(:cameras, %{}), Access.key("Canon", %{})], fn(canons) ->
  {canons, put_in(canons, [10_005], "Canon 80D")}
end)

// %{
  cameras: %{
    "Nikon" => %{
       10_001 => "Nikon D90"
    },
    "Canon" => %{
      10_004 => "Canon 50D",
      10_005 => "Canon 80D"
    }
  }
}

This solution works because the get_and_update_in/3 function can evaluate functions passed into the list in its second argument.

It can evaluate function as as well the traditional “key” values like atoms and strings.

Here’s the kicker, Access.key/2 actually returns a function. So, we are actually passing a function in the list as part of the second parameter to get_and_update_in/3 function: [Access.key(:lenses, %{})].

As you might have guessed, the second (optional) argument to Access.key/2 is the default value that should be used if the lenses key is not found.

Of course, if the key is found, it ignores the default value. But this allows us to dynamically update a map in Elixir not matter if the key exists or not.


Freelance Elixir & Ruby on Rails DeveloperHey, I’m Adam. I’m guessing you just read this post from somewhere on the interwebs. Hope you enjoyed it.

You can also follow me on the Twitters at: @DeLongShot