put_in
and update_in
can be very useful for deeply nested values in maps, but if you're trying to add keys that don't exist yet, these commands fail. Use Access.key
for this.
Let's work through a reduce example.
Input
input = [
{"Martin", :age, 31},
{"Eva", :height, 182},
{"Martin", :height, 192}
]
Target output
%{
"Martin" => %{
age: 31,
height: 192
},
"Eva" => %{
height: 182
}
}
Reduce function
Enum.reduce(input, %{}, fn {name, key, val}, acc ->
put_in(acc, [name, key], val)
end)
This will end up with a simple error: (ArgumentError) could not put/update key :age on a nil value
.
Solution
Enum.reduce(input, %{}, fn {name, key, val}, acc ->
put_in(acc, [Access.key(name, %{}), key], val)
end)
# %{"Eva" => %{height: 182}, "Martin" => %{age: 31, height: 192}}
Explanation
The Access.key command will return a function (as opposed to a calculated value) that the put_in
can later use. It hides some complexity, where it checks if acc
has the key and if not, puts in a default value (an empty map).
Access.key for keyword lists
There's no Access.key for keyword lists though. But you can write your own function for it. Or of course - just copy paste from here:
Let's try this on the same reduce with keyword lists:
input = [
{:Martin, :age, 31},
{:Eva, :height, 182},
{:Martin, :height, 192}
]
Enum.reduce(input, [], fn {name, key, val}, acc ->
put_in(acc, [access_nil.(name), key], val)
end)
Notice the dot between access_nil and (name) - we're using an anonymous function in this example (meaning the function isn't defined in a module like generally it's supposed to).
Notice also that I've converted the strings in the input to atoms. Do not convert user input to atoms. Problem is this access_nil
function only works with atoms, because keyword lists only work with atoms. In this case you implement an algorithm manually that uses stuff like List.keytake()
. But this could be very processor intensive so perhaps you can just convert your lists into maps and later back to lists?