sodapopcan
Help me rewrite these nested reductions?
I have a form with arbitrary levels of deeply nested associations. One particular association that lives a couple of levels down can have an upload associated with it. To track these I need to allow_upload based on the indices of each level. Currently this looks like this:
socket.assigns.form.source
|> Ecto.Changeset.get_assoc(:mockups)
|> Enum.with_index()
|> Enum.reduce(socket, fn {mockup, mockup_index}, socket ->
mockup
|> Ecto.Changeset.get_assoc(:elements)
|> Enum.with_index()
|> Enum.reduce(socket, fn {element, element_index}, socket ->
element
|> Ecto.Changeset.get_assoc(:transformations)
|> Enum.with_index()
|> Enum.reduce(socket, fn {_transformation, transformation_index}, socket ->
allow_image_upload(
socket,
"transformation-#{mockup_index}-#{element_index}-#{transformation_index}"
)
end)
end)
end)
I’m wondering how others would go about this.
Of course I realize I can abstract out the commonalities into a private function, but I would rather not do that as this is the only place this type of thing is happening and feel it’ll make it less readable for passers-by. I’m wondering if there is a reducer pattern that can do this type of nested thing in a flat pipeline. I have a feeling Pathex can probably come in handy here but interested in other options. I’m thinking of reducing into a 2-tuple of {socket, %{}} and building up the indices in the map then doing a final reduction to build up all the keys and allow_upload them, but that feels like it will be more complex to read.
Anyway, just wondering! As verbose as my example is, I don’t hate it as it’s pretty easy to read. Credo is complaining about the indentation depth, though, and I don’t necessarily disagree with it.
Marked As Solved
100phlecs
It’s a great tool, one of the main reasons I like Elixir.
Yes, if you need to update the socket, then you can tack on reduce:
assoc_with_index = fn changeset, assoc ->
changeset
|> Ecto.Changeset.get_assoc(assoc)
|> Enum.with_index()
end
source = socket.assigns.form.source
for {mockup, mockup_index} <- assoc_with_index.(source, :mockups),
{element, element_index} <- assoc_with_index.(mockup, :elements),
{_transformation, transformation_index} <- assoc_with_index.(element, :transformations),
reduce: socket do
socket ->
allow_image_upload(
socket,
"transformation-#{mockup_index}-#{element_index}-#{transformation_index}"
)
end
Also Liked
100phlecs
When you start dealing with nested iterative structures, for is your best bet.
assoc_with_index = fn changeset, assoc ->
changeset
|> Ecto.Changeset.get_assoc(assoc)
|> Enum.with_index()
end
source = socket.assigns.form.source
for {mockup, mockup_index} <- assoc_with_index.(source, :mockups),
{element, element_index} <- assoc_with_index.(mockup, :elements),
{_transformation, transformation_index} <- assoc_with_index.(element, :transformations) do
allow_image_upload(
socket,
"transformation-#{mockup_index}-#{element_index}-#{transformation_index}"
)
end
LostKobrakai
It also supports reduce, to allow wiring the socket state through.
sodapopcan
Beautiful! So much nicer than what I was thinking.
Thank you both!
D4no0
I would also consider an approach where I would break this down into small functions, so the main function eould be flat.
Sorc96
Personally, I probably wouldn’t bother looking into assoc_with_index, the usage makes pretty clear what it does. I’m also not saying that I would necessarily rewrite the code like this, I just wanted to show that the reduce was causing the most complexity here, in my opinion.
I guess my issue with the original code is that you need to thread the socket through all the layers in order to use it in the innermost reduce. That complicates it a lot for me, because the creation of the names and modification of the socket are mixed together. So maybe a version with a few flat_maps that generate the values first and then a reduce - basically my for example, but without the for - would be a pretty good solution as well.







