Working with Nested structures

Hi All,

I could use some help working with nested structures. I have this map:

%{
  tasks: [
    %{completed: false, date_added: "3.18.2021", id: 1, name: "walk the dog"},
    %{completed: false, date_added: "3.18.2021", id: 2, name: "cook dinner"},
    %{completed: false, date_added: "3.18.2021", id: 3, name: "laundry"}
  ]
}

I want to pass that map into function, along with an integer. If there’s a task with the same ID then the number that gets passed in, I want to update that map so that the “completed” field is changed to true and everything else is left unchanged.

Here’s what I have so far where I can update the map, but I’m not sure how to attach it back to the original list of maps.

 def complete_item(list, item_id)  do
  for items_in_list <- list[:tasks], Map.get(items_in_list, :id) == item_id, do: Map.put(items_in_list, :completed, true)
  end

Hello and welcome,

There are many ways to update/put a key in a map, Map.update, Map.put, Map.new/2, but in that particular case where You want to update an existing atom key in a map, You can use the form map = %{map | key: value}

You can do like this…

iex(1)> map = %{
  tasks: [
    %{completed: false, date_added: "3.18.2021", id: 1, name: "walk the dog"},
    %{completed: false, date_added: "3.18.2021", id: 2, name: "cook dinner"},
    %{completed: false, date_added: "3.18.2021", id: 3, name: "laundry"}
  ]
}
iex(2)> i = 1
iex(3)> map = %{map | tasks: Enum.map(map.tasks, fn 
  task when task.id==id -> %{task | completed: true}
  task -> task 
end)}
%{
  tasks: [
    %{completed: true, date_added: "3.18.2021", id: 1, name: "walk the dog"},
    %{completed: false, date_added: "3.18.2021", id: 2, name: "cook dinner"},
    %{completed: false, date_added: "3.18.2021", id: 3, name: "laundry"}
  ]
}
3 Likes

Another option is to use Kernel.update_in/3 which is intended to help with updating nested structures.

This will give you something to play with:

iex(1)> all = fn :get_and_update, data, next -> data |> Enum.map(next) |> Enum.unzip() end                                        
#Function<42.97283095/3 in :erl_eval.expr/5>
iex(2)> complete_item = fn list, id -> update_in(list, [:tasks, all], fn %{id: ^id} = t -> %{t | completed: true}; t -> t end) end
#Function<43.97283095/2 in :erl_eval.expr/5>
iex(3)> list = %{
...(3)>   tasks: [
...(3)>     %{completed: false, date_added: "3.18.2021", id: 1, name: "walk the dog"},
...(3)>     %{completed: false, date_added: "3.18.2021", id: 2, name: "cook dinner"},
...(3)>     %{completed: false, date_added: "3.18.2021", id: 3, name: "laundry"}
...(3)>   ]
...(3)> }
%{
  tasks: [
    %{completed: false, date_added: "3.18.2021", id: 1, name: "walk the dog"},
    %{completed: false, date_added: "3.18.2021", id: 2, name: "cook dinner"},
    %{completed: false, date_added: "3.18.2021", id: 3, name: "laundry"}
  ]
}
iex(4)> complete_item.(list, 2)
%{
  tasks: [
    %{completed: false, date_added: "3.18.2021", id: 1, name: "walk the dog"},
    %{completed: true, date_added: "3.18.2021", id: 2, name: "cook dinner"},
    %{completed: false, date_added: "3.18.2021", id: 3, name: "laundry"}
  ]
}
2 Likes

The way your list is structured is non-performant, because in order to find an ID you may have to traverse the whole list, and there could be duplicates.

If you convert tasks into a map being the map key the id of the task, you can do this much easier with (as @globalkeith suggested) Kernel.update_in/2 (note it is /2 in this case)

map = %{
  tasks: %{
    1 => %{completed: false, date_added: "3.18.2021", name: "walk the dog"},
    2 => %{completed: false, date_added: "3.18.2021", name: "cook dinner"},
    3 => %{completed: false, date_added: "3.18.2021", name: "laundry"}
  }
}

complete_item =
  fn(map, id) ->
    {_completed, update_map} = get_and_update_in(
      map[:tasks][id][:completed], fn completed -> {completed, true} end
    )
    update_map
  end

map = complete_item.(map, 1)

So your complete_item function could be rewritten like

def complete_item(map, id) do
  get_and_update_in(map[:tasks][id][:completed], &{&1, true})
  |> elem(1)
end
4 Likes

Awesome, this is all really helpful, I appreciate it! @eksperimental - totally makes sense. I was considering changing the structure up because of that.

Thanks for the feedback!

1 Like