codeanpeace

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

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

codeanpeace

Ahh of course, for some reason didn’t occur to just pass in the ids. Anyways, thanks @al2o3cr and thanks Elixir for first class functions!

gregvaughn

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

Where Next?

Popular in Questions Top

Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New

We're in Beta

About us Mission Statement