# Best way to perform this map reduction in idiomatic elixir

Hi all, I have this function requirement:

``````@spec group_by_chunk_index([MyStruct]) :: %{
required(integer()) => [MyStruct]
}
``````

Which takes in a list of structs and groups them into a map where the keys are an integer representing some transformation over a struct field. For example, my struct has the field `weights` which is `[integer()]`. I want to loop over these structs, extract the weights for each one, transform those weight values, and group them into a new map using the transformed values as keys. For example:

``````first = MyStruct.new(weights: )
second = MyStruct.new(weights: )
third = MyStruct.new(weights: )

wanted = %{
1000 => [first, second],
2000 => [third]
}

assert group_by_chunk_index([first, second, third]) == wanted
``````

Where any weight in the range [0-5) is mapped to the number 1000, [5, 10) to the number 2000, etc. My current approach involves an ugly series of Enum.reduce calls with a map as an accumulator. The only functions I use are Enum.reduce and Map.get_and_update. Is there a more functional approach I can take here perhaps using some other special operations from the Enum or List libraries? Thank you in advance.

Hello and welcome,

There is a good candidate… Enum.group_by, but You don’t really want to group by index, but by the resulting number.

Well, this describe all functions, because it’s all about transforming input to output Assuming You might want the sum…

``````Enum.group_by([first, second, third], & weights_to_number(Enum.sum(&1.weights)), & &1)
``````

and You need to write weights_to_number, which take the sum (change to whatever transformation) of weights.

``````def weights_to_number(sum) when sum in 0..5, do: 1000
etc
``````

I used to add new inside my modules, but now I prefer %MyStruct{weights: }

``````iex> list = [%{weights: }, %{weights: }, %{weights: }]
iex> w_to_n = fn x when x in 0..5 -> 1000; _ -> 2000 end
iex> Enum.group_by(list, &w_to_n.(Enum.sum(&1.weights)), & &1)
%{1000 => [%{weights: }, %{weights: }], 2000 => [%{weights: }]}
``````
1 Like

``````list_of_structs = [first, second, third]
``````

Convert it to a list of `{weight, struct}` pairs. `weights` is a list, so I assume can it have multiple values.

``````list_of_pairs = Enum.flat_map(list_of_structs, fn struct -> Enum.map(struct.weights, &{&1, struct}) end)
``````

Transform the weight values to the output format:

``````list_of_transformed_pairs = Enum.map(list_of_pairs, fn {w, s} -> {transform_weight(w), s} end)
``````

Now you’ve got a list of tuples like `{1000, first}`. This is a case where the third argument to `Enum.group_by` is handy: the output is grouped by the first element of the tuple, but the value grouped is the second:

``````map_of_lists = Enum.group_by(list_of_transformed_pairs, fn {w, _} -> w end, fn {_, s} -> s end)
``````
2 Likes

This worked really well! Thanks for the tips, learned a ton

I love these sorts of exercises. This one works nicely with a for comprehension. I just threw something together quickly in iex, but you should be able to translate it to named named functions in a module.

``````# define struct inline
iex(14)> defmodule MyStruct, do: defstruct [weights: []]
{:module, MyStruct,
<<70, 79, 82, 49, 0, 0, 6, 184, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 185, ...

# here's the three inputs
iex(15)> {first, second, third} = {%MyStruct{weights: }, %MyStruct{weights: }, %MyStruct{weights: }}
{%MyStruct{weights: }, %MyStruct{weights: }, %MyStruct{weights: }}

# This is the "binning" function. Yours will likely be more complex and a named function in a module.
iex(16)> binner = &if(&1 in 0..4, do: 1000, else: 2000)
#Function<7.126501267/1 in :erl_eval.expr/5>

# Here's the meat of the code. We pattern match the `weights` field and name the full struct `s`.
# We reduce into a Map and on each weight call `Map.update` to either add a single item
# list if it is a new key, or else prepend to the list if the key already exists.
iex(17)> for %{weights: ws} = s <- [first, second, third], w <- ws, reduce: %{}, do: (m -> Map.update(m, binner.(w), [s], &[s | &1]))
%{
1000 => [%MyStruct{weights: }, %MyStruct{weights: }],
2000 => [%MyStruct{weights: }]
}
``````
3 Likes