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

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
Tee
can someone please explain to me how Enum.reduce works with maps
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
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 49522 488
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
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
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement