Hi, I am probably missing something fundamental of Elixir knowledge, but I am not able to add nor update values of elements in deeply nested list of maps structured like this:
Ok gentlemen, all your solutions above work great and I have to seriously thank you for making my day Now I’m wondering, how to get familiar with those kind of operations without asking for a magical help of you wizards every time. Probably it could be a good start to get familiar with anonymous functions.
It’s still “trivial issue”, but don’t worry … such things happens. There are many ways how to solve a “trivial issue” when you are lost:
Ask on forum as you did
Read documentation again
Try to simplify and isolate issue
My favourite and also the simplest one is: go to toilet (i.e. just relax)
Looks like you understand Elixir enough to solve your problem. You remember about Access.all() and just forgot to use it in all required places. You just need to chill out and take a look at your code later with fresh mind.
Let me explain everything. Let’s start from what you have tried:
# a variable which would be manipulated - simple
original_animals
# map (i.e. update) each item of `list` called `group` - also simple
|> Enum.map(fn group ->
# here is a mistake …
# get items value - you forget about group
# we map group and returning only the list of it's changed items
# most probably such mistake comes from OOP language
group.items
# as above map (i.e. update) each item of `list` called `item`
|> Enum.map(fn item ->
# if there is only one call then using pipe does not makes sense
item
# simply put key in value
|> Map.put(NEW_KEY, NEW_VAL)
# end of map`item`
end )
# end of map `group`
end )
# a variable which would be manipulated - simple
original_animals
# here your path requests for a `NEW_KEY` inside a `list` which obviously does not works
# as list is not a map
# you just forgot about calling `Access.all()` again for `items` list
# however it's not the only problem …
# you want to return a tuple of `{old_list, NEW_VAL}` - you intended to `{old_map, NEW_VAL}`
# but both are wrong - what you need here is to update `old_map` and not wrap it in `tuple`
|> get_and_update_in([Access.all(), :items, NEW_KEY], &{&1, NEW_VAL})
# you wrongly expect that `case` would magically update
# however the `update` already happen - list is mapped to `{old_list, NEW_VALUE}`
# you do not need `original` so `get_and_update_in` is bad by assumption
# therefore you wanted `update_in`
|> case do {_original, updated_animals} ->
updated_animals
end
Now let’s take a look at answers. Personally I prefer @mudasobwa as it’s solution is easiest and shortest.
# as above `update_in` is what you need
update_in(
# simply your variable
original_animals,
# also as above here is your missed `Access.all()` call on `items`
[Access.all(), :items, Access.all()],
# the update function gets each element of list in items
# and puts a new key with a new value to element
&Map.put(&1, :foo, :bar)
)
@kokolegorille’s solution is a bit overcomplicated. Let’s check what he is doing …
# easy start
original_animals
# as in your first example - `m` here is called be you `group`
|> Enum.map(fn m ->
# @kokolegorille also forgot about `Access.all()`
# however he understand how to update every item in group.items list
# first of all he is not updating - he is just putting - in this case both putting and updating works
# as a new value of `items` i.e. new `items`
# he maps every item of list
# and as @mudasobwa simply puts a value under key
put_in(m, [:items], Enum.map(m.items, & Map.put(&1, :key, :value)))
end)
# start as always
original_animals
# map as above
|> Enum.map(fn m ->
# use `%{map | existing_key: new_value}` syntax instead of `put_in`
# while both works `put_in` is easier in this case
# the rest did not changed
# you returned only changed `items` instead of updated `group`
# but he is updating `m` (i.e. `group`) and map each element of `items` list
%{m | items: Enum.map(m.items, & Map.put(&1, :key, :value))}
end)
We can “merge” both answers into one:
# as @kokolegorille we are going to use put_in instead of update_in
put_in(
# the variable to manipulate
original_animals,
# as @mudasobwa we are using `Access.all()` including the one you have missed at end
# but also we can add a `key` to our path, so we do not need to call `Map.put`
[Access.all(), :items, Access.all(), :key],
# a new value
:value
)
Now there is only one problem … When to use put_in and when to use update_in. We want put_in in nested structure in case we want to simply put same value regardless of content whereas update_in allows you to pattern match every item map and update it based on it’s values.
Thank you Eiji for such a complex explanation. You are absolutely right in all given points. With it and those helpful resources, the issue with map updating seems to be almost trivial again.
I marked kokolegorilles answer as a solution because with the Access.all() way there seems to be an error when interacting with Phoenix schema data:
function App.Context.Schema.get_and_update/3 is undefined
(App.Context.Schema does not implement the Access behaviour)
Should be the mudasobwa’s answer the right solution?
Anyway, hats off to Elixirforum community. Thank you all guys!
Unlike plain map your custom struct (Ecto.Schema defines custom schema struct) does not have an Access implementation and therefore you cannot access keys in it dynamically like in maps.
defmodule Example do
defstruct [:sample]
end
iex> %{sample: 5}[:sample]
5
iex> %Example{sample: 5}[:sample]
** (UndefinedFunctionError) function Example.fetch/2 is undefined (Example does not implement the Access behaviour)
Example.fetch(%Example{sample: 5}, :sample)
(elixir 1.12.0-dev) lib/access.ex:285: Access.get/3
To solve it you need to implement Access behaviour. However you do not need to do it by hand all the time. You can use accessible library.
defmodule Example do
defstruct [:sample]
use Accessible
end
iex> %{sample: 5}[:sample]
5
iex> %Example{sample: 5}[:sample]
5
We could have few solutions for one problem. The one who should choose which one is best for you is … you.