Enumerables - how does Enum.reduce work with maps

can someone please explain to me how Enum.reduce works with maps

1 Like

Using Enum.reduce ultimately delegates to the protocol function Enumerable.reduce, which is implemented for Map here:

This first converts the Map to a list of {key, value} tuples, then reduces over it.

8 Likes

it would be nice with an example that i can relate with

alphabet_positions = {"a" => 1, "b" => 2, "c" => 3, ...}
Enum.reduce(alphabet_positions, 0, fn({letter, number}, accumulator) ->
  if "aeiou" =~ letter do
    accumulator += number
  end
  accumulator
end)

This trivial example adds up the positions of all the vowels in the alphabet.

Maps implement the Enumerable protocol. That implementation turns the map into a list of key-value tuples - see Map.to_list/1 (the actual implementation uses the Erlang version).

What is being reduced is the resulting list of key-value tuples, not the map itself - which goes something like this:

iex(1)> sum_values = fn {_k, v}, sum -> v + sum end
#Function<13.91303403/2 in :erl_eval.expr/5>
iex(2)> collect_keys = fn {k, _v}, others -> [k | others] end
#Function<13.91303403/2 in :erl_eval.expr/5>
iex(3)> do_both = fn {k, v}, {keys, sum} -> {[k | keys], v + sum} end 
#Function<13.91303403/2 in :erl_eval.expr/5>
iex(4)> map = %{"a" => 1, "b" => 2}
%{"a" => 1, "b" => 2}
iex(5)> keyword_list = Map.to_list(map)
[{"a", 1}, {"b", 2}]
iex(6)> values_result = List.foldl(keyword_list, 0, sum_values)
3
iex(7)> keys_result = List.foldl(keyword_list, [], collect_keys)
["b", "a"]
iex(8)> result = List.foldl(keyword_list, {[],0}, do_both)
{["b", "a"], 3}
iex(9)> 

Edit: Correction as indicated by @NobbZ

That won’t work, there is no += in elixir. I think what you really mean is more like this:

alphabet_positions = %{"a" => 1, "b" => 2, "c" => 3, …}
Enum.reduce(alphabet_positions, 0, fn {letter, position}, sum ->
  if letter in ~w[a e i o u], do: sum + position, else: sum
end)
2 Likes

No, its turned into a list of tuples with the key as the first element and the corresponding values as the second. The implementation of Map.to_list/1 is equivalent to this:

map
|> Map.keys()
|> Enum.reduce([], fn key, list -> [{key, map[key]} | list] end)
2 Likes

Looks like it uses an internal iterator to avoid redundant traversals:

Have tried this and the result is true
which can also be implented this way:
Enum.reduce(%{“a” => 1, “b” => 2}, [], fn {k, v}, acc -> [{k, v} | acc] end)
[{“b”, 2}, {“a”, 1}]

your example returns the accumalator of the last vowel which i understand it will be the initial position of the last vowel added to accumulator giving you 6

Going the other way (list to map) Map.new/2 can be helpful.

iex(1)> square_tuple = fn x -> {x , x * x} end
#Function<7.91303403/1 in :erl_eval.expr/5>
iex(2)> result = Map.new(1..10, square_tuple)
%{
  1 => 1,
  2 => 4,
  3 => 9,
  4 => 16,
  5 => 25,
  6 => 36,
  7 => 49,
  8 => 64,
  9 => 81,
  10 => 100
}
iex(3)>
1 Like
accumulator += number

this is a syntax error in elixir

You’re right. I’ve been writing too much JavaScript recently. :laughing:

Tangentially related, I love the StackOverflow Axiom at play here: “It’s easier to get a good answer by proposing an incorrect implementation than by asking a well-stated question.”

4 Likes