How do I map and group_by at the same time?
Since Elixir 1.3 there is now Enum.group_by/3
that takes a mapper_fun
argument, which solves exactly this problem:
Enum.group_by(enumerable, &elem(&1, 0), &elem(&1, 1))
Obsolete answer:
At this moment, there is no such function in the standard library. I ended up using this:
def map_group_by(enumerable, value_mapper, key_extractor) do
Enum.reduce(Enum.reverse(enumerable), %{}, fn(entry, categories) ->
value = value_mapper.(entry)
Map.update(categories, key_extractor.(entry), [value], &[value | &1])
end)
end
which can (for my example) then be called like this:
map_group_by(
collection,
fn {_, second} -> second end,
fn {first, _} -> first end
)
It is adapted from the standard library's Enum.group_by
.
Regarding the [value]
: I don't know what the compiler can or cannot optimize, but at least this is what Enum.group_by
does as well.
Note the Enum.reverse
call, which was not in the example from my question. This ensures that the element order is preserved in the resulting value lists. If you do not need that order to be preserved (like I did in my case, in which I only wanted to sample from the result anyway), it can be dropped.
Real answer
Since Elixir 1.3 there is now Enum.group_by/3
who's 3rd argument is a function that gets mapped over the keys.
Obsolete Answer
But I'll give you my solution:
To start off, It's important to notice, as you see in Elixir Docs that a list of tuples is the same as a key-value list:
iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true
So with this in mind it's easy to use the Enum.map
across it.
This does make two passes it it but it's a little cleaner looking than what you had:
defmodule EnumHelpers do
def map_col(lst) do
lst
|> Enum.group_by(fn {x, _} -> x end)
|> Enum.map(fn {x, y} -> {x, Dict.values y} end)
end
end
IO.inspect EnumHelpers.map_col([a: 2, a: 3, b: 3])
which will print out:
[a: [3, 2], b: [3]]
Edit: Faster Version:
defmodule EnumHelpers do
defp group_one({key, val}, categories) do
Dict.update(categories, key, [val], &[val|&1])
end
def map_col_fast(coll) do
Enum.reduce(coll, %{}, &group_one/2)
end
end
IO.inspect EnumHelpers.map_col_fast([a: 2, a: 3, b: 3])