Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Efficient way to filter a Map by value in Elixir

In Elixir, what would be an efficient way to filter a Map by its values.

Right now I have the following solution

%{foo: "bar", biz: nil, baz: 4}
|> Enum.reject(fn {_, v} -> is_nil(v) end)
|> Map.new

This solution seems pretty inefficient to me. When called on a Map, Enum.reject/2 returns a Keywords. Since I want a Map, I need to call Map.new/1 to convert that Keywords back to me.

This seems inefficient because Enum.reject/2 has to iterate over the Map once and then presumably, Map.new/1 has to iterate over the Keywords another time.

What would be a more efficient solution?

like image 538
Boris Bera Avatar asked May 23 '17 22:05

Boris Bera


3 Answers

You can use :maps.filter/2, which filters a map and does not create any intermediate list:

iex(1)> :maps.filter fn _, v -> v != nil end, %{foo: "bar", biz: nil, baz: 4}
%{baz: 4, foo: "bar"}

A simple benchmark confirms this is faster than Enum.filter + Map.new:

map = for i <- 1..100000, into: %{}, do: {i, Enum.random([nil, 1, 2])}

IO.inspect :timer.tc(fn ->
  map
  |> Enum.reject(fn {_, v} -> is_nil(v) end)
  |> Map.new
end)

IO.inspect :timer.tc(fn ->
  :maps.filter fn _, v -> v != nil end, map
end)
{44728,
 %{48585 => 1, 60829 => 2, 12995 => 1, 462 => 2, 704 => 2, 28954 => 2,
   29635 => 2, 78798 => 1, 92572 => 1, 89750 => 2, 39389 => 2, 62855 => 2,
   79313 => 1, 92062 => 2, 61871 => 1, 92856 => 2, 75920 => 1, 59922 => 1,
   37912 => 2, 30420 => 2, 51211 => 2, 7994 => 2, 78269 => 2, 9765 => 2,
   38352 => 2, 6653 => 1, 82555 => 2, 54031 => 2, 45138 => 1, 41351 => 1,
   40746 => 1, 5961 => 1, 66704 => 2, 33823 => 1, 47603 => 1, 86873 => 1,
   81009 => 2, 96255 => 1, 36219 => 1, 1328 => 2, 33314 => 1, 54477 => 2,
   40189 => 2, 27028 => 1, 31676 => 1, 94037 => 1, 32388 => 1, 4351 => 1,
   46309 => 1, ...}}
{28638,
 %{48585 => 1, 60829 => 2, 12995 => 1, 462 => 2, 704 => 2, 28954 => 2,
   29635 => 2, 78798 => 1, 92572 => 1, 89750 => 2, 39389 => 2, 62855 => 2,
   79313 => 1, 92062 => 2, 61871 => 1, 92856 => 2, 75920 => 1, 59922 => 1,
   37912 => 2, 30420 => 2, 51211 => 2, 7994 => 2, 78269 => 2, 9765 => 2,
   38352 => 2, 6653 => 1, 82555 => 2, 54031 => 2, 45138 => 1, 41351 => 1,
   40746 => 1, 5961 => 1, 66704 => 2, 33823 => 1, 47603 => 1, 86873 => 1,
   81009 => 2, 96255 => 1, 36219 => 1, 1328 => 2, 33314 => 1, 54477 => 2,
   40189 => 2, 27028 => 1, 31676 => 1, 94037 => 1, 32388 => 1, 4351 => 1,
   46309 => 1, ...}}
like image 128
Dogbert Avatar answered Nov 16 '22 00:11

Dogbert


Since Elixir version 1.13, Map.filter(map, fun) exists natively. See https://hexdocs.pm/elixir/Map.html#filter/2

like image 44
Istopopoki Avatar answered Nov 16 '22 00:11

Istopopoki


In this case comprehension would be a good idea, because it also does not create an intermediate list and returns you a map:

map = %{baz: 4, biz: nil, foo: "bar"}
for {key, value} <- map, !is_nil(value), into: %{}, do: {key, value}
like image 3
PatNowak Avatar answered Nov 16 '22 01:11

PatNowak