Like @LostKobrakai mentioned, scoping is one problem: code inside a do
block can’t rebind variables outside of that block in a way that’s visible outside.
The other problem is that ++
makes a new list, it doesn’t mutate either argument. Code like what you posted is pretty common in other languages:
# in Ruby
list_a = []
list_b = []
list_c.each do |item|
item_a, item_b = process(item)
list_a.concat(item_a.inner_list)
list_b.concat(item_b.inner_list)
end
# in Python
list_a = []
list_b = []
for item in list_c:
item_a, item_b = process(item)
list_a.extend(item_a.inner_list)
list_b.extend(item_b.inner_list)
This approach will not work in Elixir. My suggestion is that you temporarily forget you even saw Enum.each
; it’s only useful in a very narrow set of situations.
A better way to think about solving these kinds of problems in Elixir is to focus on how the “shape” of the data changes.
As a first step, we want to run process/1
on every element of list_c
:
processed_list = Enum.map(list_c, &process/1)
Each element of processed_list
is a tuple shaped like {item_a, item_b}
.
There are several possibilities here: we could refine these tuples by extracting inner_list
, but instead we’ll start by regrouping them.
What we have is a list of 2-element tuples.
What we want is a 2-element tuple of lists (what the original called list_a
and list_b
)
This kind of transformation comes up often enough that it has a name: Enum.unzip/1
. Using it looks like:
tuple_of_lists = Enum.unzip(processed_list)
Now tuple_of_lists
is shaped like {list_of_item_as, list_of_item_bs}
Finally, we need to extract the inner_list
parts. We could use Enum.map
again:
# not quite right - see below for a correct solution
{list_of_as, list_of_bs} = tuple_of_lists
list_a = Enum.map(list_of_as, & &1.inner_list)
list_b = Enum.map(list_of_bs, & &1.inner_list)
but this doesn’t quite do what we’re looking for: it results in list_a
and list_b
being lists of lists. You could use List.flatten/1
, if the values in inner_list
are not themselves lists.
BUT
There’s an easier way! Again, what we’re looking for is referenced enough to have a name: Enum.flat_map
:
{list_of_as, list_of_bs} = tuple_of_lists
list_a = Enum.flat_map(list_of_as, & &1.inner_list)
list_b = Enum.flat_map(list_of_bs, & &1.inner_list)
Putting all the pieces together gives this code:
processed_list = Enum.map(list_c, &process/1)
tuple_of_lists = Enum.unzip(processed_list)
{list_of_as, list_of_bs} = tuple_of_lists
list_a = Enum.flat_map(list_of_as, & &1.inner_list)
list_b = Enum.flat_map(list_of_bs, & &1.inner_list)
This code can be tidied up with some Elixir syntax goodies:
# squish "processed_list" out of existence since it is bound and then immediately used
tuple_of_lists =
list_c
|> Enum.map(&process/1)
|> Enum.unzip()
{list_of_as, list_of_bs} = tuple_of_lists
list_a = Enum.flat_map(list_of_as, & &1.inner_list)
list_b = Enum.flat_map(list_of_bs, & &1.inner_list)
and even more with then
which was recently added:
{list_a, list_b} =
list_c
|> Enum.map(&process/1)
|> Enum.unzip()
|> then(fn {list_of_as, list_of_bs} ->
{
Enum.flat_map(list_of_as, & &1.inner_list),
Enum.flat_map(list_of_bs, & &1.inner_list)
}
end)
This code does exactly the same steps as the first “un-simplified” version, but avoids having to name a bunch of variables that are used exactly once (like processed_items
and tuple_of_lists
).