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])

Tags:

Elixir