Reputation: 24888
What is an efficient way to find the key associated with the list of minimum length, in a map of lists in Elixir. Let's say I have:
z = %{a: [1, 2, 3], b: [4, 5], c: [6, 7, 8, 9]}
I know I can do:
Enum.map z, fn {k, v} -> length(v) end
which will give me:
[3, 2, 4]
But what I really need is just the answer, namely the key associated with the minimum value 2, which of course is :b.
I will be running this on dynamic map of lists approximately every second so I'd like it to be as efficient as possible.
Upvotes: 1
Views: 1245
Reputation: 24888
For anyone who wishes to repeat @Dogbert's benchmark, including the answer of Patrick Oscity:
map = for(key <- 1..1000, into: %{}, do: {key, Enum.random([[1, 2, 3], [4, 5], [6, 7, 8, 9]])})
Benchee.run(%{
"mudasobwa" => fn ->
Enum.reduce(map, {nil, 0}, fn
{k, v}, {_, len} when len == 0 or len > length(v) -> {k, length(v)}
_, acc -> acc
end)
|> elem(0)
end,
"dogbert" => fn ->
:maps.fold(
fn k, v, {_, min} = acc ->
l = length(v)
if min == 0 || l < min, do: {k, l}, else: acc
end,
{nil, 0},
map
)
|> elem(0)
end,
"oscity" => fn ->
map
|> Enum.min_by(fn {_k, v} -> length(v) end)
|> elem(0)
end
})
My results on an Nvidia TX2 (64-bit ARM):
Name ips average deviation median 99th %
oscity 252.78 3.96 ms ±3.01% 3.92 ms 4.38 ms
mudasobwa 80.66 12.40 ms ±1.61% 12.37 ms 13.15 ms
dogbert 32.90 30.39 ms ±0.90% 30.32 ms 31.27 ms
Benchee code is here:
https://hex.pm/packages/benchee
ie just add
{:benchee, "~> 0.12.1"}
under deps in mix.exs.
Upvotes: 0
Reputation: 54674
I prefer simplicity over performance unless absolutely necessary, so here's how I would do it, if performance was less of a concern:
map
|> Enum.min_by(fn {_k, v} -> length(v) end)
|> elem(0)
Adding to the benchmark by @Dogbert reveals that this is ~1.5x slower with an avg. 90µs/iteration on my machine. That's still plenty of headroom to run it every second.
Upvotes: 5
Reputation: 222040
If you're looking for speed, :maps.fold
would usually be a tiny bit faster than Enum.reduce
. Here's an implementation which runs ~10% faster than @mudasobwa's implementation:
:maps.fold(
fn k, v, {_, min} = acc ->
l = length(v)
if min == 0 || l < min, do: {k, l}, else: acc
end,
{nil, 0},
map
)
|> elem(0)
Benchmark code:
map = for(key <- 1..1000, into: %{}, do: {key, Enum.random([[1, 2, 3], [4, 5], [6, 7, 8, 9]])})
Benchee.run(%{
"mudasobwa" => fn ->
Enum.reduce(map, {nil, 0}, fn
{k, v}, {_, len} when len == 0 or len > length(v) -> {k, length(v)}
_, acc -> acc
end)
|> elem(0)
end,
"dogbert" => fn ->
:maps.fold(
fn k, v, {_, min} = acc ->
l = length(v)
if min == 0 || l < min, do: {k, l}, else: acc
end,
{nil, 0},
map
)
|> elem(0)
end
})
Output:
Name ips average deviation median 99th %
dogbert 12.36 K 80.90 μs ±25.05% 73 μs 146.20 μs
mudasobwa 11.14 K 89.81 μs ±34.32% 80 μs 185 μs
Comparison:
dogbert 12.36 K
mudasobwa 11.14 K - 1.11x slower
Upvotes: 2
Reputation: 120990
The most efficient way to do anything in Elixir is to Enum.reduce/3
:
Enum.reduce(z, {nil, 0}, fn
{k, v}, {_, len} when len == 0 or len > length(v) -> {k, length(v)}
_, acc -> acc
end)
#⇒ {:b, 2}
To get :b
only, pattern match the result:
{key, _} = ⇑the above⇑
or (worse) pipe to |> Tuple.to_list() |> List.first
.
Here we update the accumulator if:
Upvotes: 1