Having hard time understanding Enum.reduce()

I have two ecto structs.

categories = [
  %MyApp.Category{
    id: 1,
    name: "Electronics",
    products: #Ecto.Association.NotLoaded<association :products is not loaded>
  },
  %MyApp.Category{
    id: 2,
    name: "Furniture",
    products: #Ecto.Association.NotLoaded<association :products is not loaded>
  }
]

products = [
  %MyApp.Product{
    id: 1,
    name: "TV",
    category_id: 1
  },
  %MyApp.Product{
    id: 2,
    name: "Computer",
    category_id: 1
  },
  %MyApp.Product{
    id: 3,
    name: "Office Desk",
    category_id: 2
  }
]

And I want to put products that matches category id into categories list

categories = [
  %MyApp.Category{
    id: 1,
    name: "Electronics",
    products: [
        products = [
          %MyApp.Product{
            id: 1,
            name: "TV",
            category_id: 1
          },
          %MyApp.Product{
            id: 2,
            name: "Computer",
            category_id: 1
          }
        ]

  },
  %MyApp.Category{
    id: 2,
    name: "Furniture",
    products: [
         %MyApp.Product{
            id: 3,
            name: "Office Desk",
            category_id: 2
         }
    ]
  }
]

Enum.reduce(products, categories, fn(product, cat) ->                                                                                         
 Enum.find(cat, fn c -> c.id == product.category_id end) |> Map.get_and_update!(:products, fn prev -> {prev, product} end)
end)

But no luck.
I got data from Repo.query and building from Repo.load. So I don’t want to do another database query.
How can I do this?

Your reduce strategy is actually correct, but you’re forgetting that elixir data are immutable under the hood. What your code does now is it tries to find the category, and then mutably update the products part of it

Moreover, your reduction function should basically output the same datatype you’re giving it as the accumulator. Right now, your code gives “categories” (which is a list of Product structs) in and the last term in that function is a single Product struct, so the output type has changed, which you probably didn’t intend to do.

Probably what you want to do is “replace the item you are looking for”. There is no “Enum.replace”, but what you can do is an Enum.map over categories, where if the category doesn’t match, you output the identity, but if the category does match, you update the map as you are doing now.

Also really you should be using the database to do this, probably with a preload directive, which will do all of the work for you and get rid of those products: #Ecto.Association.NotLoaded<association :products is not loaded> things.

Good luck!

1 Like

As per the suggestion from @ityonemo, use a map over categories rather than a reduce as you aren’t really changing the shape of categories, just adding things to it. Something like (untested):

 categories = Enum.map(categories, fn cat ->
   cat_products = Enum.filter(products, fn p -> p.category_id == cat.id end)

  %{cat | products: cat_products}
 end)

… or, indeed, add an option to your database Ecto code to preload products and save yourself the trouble of doing it by hand.

3 Likes

Thanks you for your advice.
Yes I know I had to use Ecto, preload, But my query is not supported in ecto.
That’s why I am doing Repo.query amd Repo.load manually.
I will try your suggestion!

1 Like

oh actually your suggestion is better (and probably what I would have implemented naively),

I was suggesting something like:

Enum.reduce(products, categories, fn product, categories →
Enum.map(categories, fn category →
if category.id == product.id do
add_product_to_category(category)
else
category
end
end)

^^ don’t do it this way.

1 Like

You might also use Enum.group_by…

iex> grouped = products |> Enum.group_by(& &1.category_id)
iex> categories |> Enum.map(& %{&1 | products: Map.get(grouped, &1.id)})
[
  %{
    id: 1,
    name: "Electronics",
    products: [
      %{category_id: 1, id: 1, name: "TV"},
      %{category_id: 1, id: 2, name: "Computer"}
    ]
  },
  %{
    id: 2,
    name: "Furniture",
    products: [%{category_id: 2, id: 3, name: "Office Desk"}]
  }
]
1 Like

I started trawling the Enum docs after writing my initial reply. You can do a lot with filter/find/map/reduce, but there’s some pretty cool stuff in there to make our lives easier!

3 Likes

Yes, Enum is really powerful, and group_by is one these cool stuff :slight_smile:

2 Likes

Thank you guys!! @ityonemo, @mindok, @kokolegorille
Now I can sleep!