codeanpeace
Help composing/abstracting list of keys to traverse nested data structures using the Access module
Hello, I’m looking to dry up some LiveView handle_info/event callbacks that require modifying a nested data structure using update_in/3 in response to CRUD, or rather CUD, actions. The second parameter for update_in/3 is a list of keys that are often functions from the Access module e.g. Access.key!/2 and Access.filter/2.
For the sake of a concrete example, Person has many Pets, Pet has many Toys, and there are callbacks that update a preloaded person struct assigned to the socket after a user adds, updates, and/or deletes a toy. For the add and delete callbacks, update_in/3 only has to reach into the toys list whereas the update callback goes a step further to find the specific toy within in the toy list.
How would I go about abstracting out the common parts of the traverse path?
person =
%Person{
pets: [
%Pet{
toys: [
%Toy{}
]
}
]
}
# within toy created callback
update_in(
person,
[
Access.key!(:pets),
Access.filter(&(&1.id == pet_id)),
Access.key!(:toys)
],
fn toys -> [toy | toys] end
)
# within toy deleted callback
update_in(
person,
[
Access.key!(:pets),
Access.filter(&(&1.id == pet_id)),
Access.key!(:toys)
],
fn toys -> Enum.reject(toys, &(&1.id == toy_id)) end
)
# within toy updated callback
update_in(
person,
[
Access.key!(:pets),
Access.filter(&(&1.id == pet_id)),
Access.key!(:toys),
Access.filter(&(&1.id == toy_id))
],
fn toy -> %{toy | new_attributes} end
)
# composing list of keys within toy updated callback
update_in(
person,
[
Access.key!(:pets),
Access.filter(&(&1.id == pet_id)),
Access.key!(:toys)
] ++ [Access.filter(&(&1.id == toy_id))],
fn toy -> %{toy | new_attributes} end
)
As demonstrated above, the first three elements in the second parameter list of keys are shared across all three callbacks. Is there a sensible way to refactor and abstract that out? I imagine some form of macros/quote/unquote would be involved… would the added brevity even be worth it given the increased complexity?
Marked As Solved
al2o3cr
update_in/3 and friends are plain functions, and their argument is a plain list (of anonymous functions). The macro-sorcery might be necessary to do things with update_in/2, but not here.
You could do this with functions:
defp toys_for(pet_id) do
[
Access.key!(:pets),
Access.filter(&(&1.id == pet_id)),
Access.key!(:toys)
]
end
# in the created/deleted callbacks:
update_in(
person,
toys_for(pet_id),
fn toys -> [toy | toys] end
)
# in the updated callback:
update_in(
person,
toys_for(pet_id) ++ [Access.filter(&(&1.id == toy_id))],
fn toy -> %{toy | new_attributes} end
)
You could also split this differently - combine Access.key!(:pets) with the Access.filter and name it pet_with_id or similar.
Also Liked
codeanpeace
gregvaughn
This is probably in the realm of chrome plating, but you could even generalize to
defp access_key_with_id(key, id) do
[
Access.key!(key),
Access.filter(&match?(^id, &1.id)),
]
end
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex









