Pattern Matching Empty Maps with Elixir

Unlike lists, using an empty map in pattern matching does not only match empty maps; it matches empty and populated maps.

> [] = [1,2,3]
** (MatchError) no match of right hand side value: [1, 2, 3]

> %{} = %{a: 1, b: 2, c: 3}
%{a: 1, b: 2, c: 3}

This was a bit of a surprise for me when attempting to match an empty list, so I wanted to share how you can pattern match an empty map (aka re-write the answers I found on this StackOverflow post).

There appear to be two primary options for matching on an empty list.

  1. You can use == and compare it to an empty map.
def my_fun(map) when map == %{}
  1. You can use the map_size/1 guard clause and compare it to 0.
def my_fun(map) when map_size(map) == 0

I was curious if there was a performance implication to either choice, so I wrote a benchmark script to compare the options.

Mix.install([:benchee])

defmodule EmptyMapMatch do
  def using_size(map) when map_size(map) == 0, do: map
  def using_size(map), do: map
  def empty_map(map) when map == %{}, do: map
  def empty_map(map), do: map
end

Benchee.run(
  %{
    "checking size" => fn input -> EmptyMapMatch.using_size(input) end,
    "matching map" => fn input -> EmptyMapMatch.empty_map(input) end
  },
  inputs: %{
    "Empty" => %{},
    "Small" => Enum.zip(1..100, 1..100) |> Enum.into(%{}),
    "Medium" => Enum.zip(1..1_000, 1..1_000) |> Enum.into(%{}),
    "Large" => Enum.zip(1..10_000, 1..10_000) |> Enum.into(%{})
  },
  time: 10
)

This script will use benchee to compare functions with map_size and == guard clauses on four different size maps.

After a few runs with different times, I found the options were equivalent speed-wise. Here is a sample result from a ten-second benchmark.

Operating System: macOS
CPU Information: Intel(R) Core(TM) i7-4771 CPU @ 3.50GHz
Number of Available Cores: 8
Available memory: 32 GB
Elixir 1.12.3
Erlang 24.1.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 10 s
memory time: 0 ns
parallel: 1
inputs: Empty, Large, Medium, Small
Estimated total run time: 1.60 min

Benchmarking checking size with input Empty...
Benchmarking checking size with input Large...
Benchmarking checking size with input Medium...
Benchmarking checking size with input Small...
Benchmarking matching map with input Empty...
Benchmarking matching map with input Large...
Benchmarking matching map with input Medium...
Benchmarking matching map with input Small...

##### With input Empty #####
Name                    ips        average  deviation         median         99th %
checking size        7.51 M      133.13 ns ±17314.76%           0 ns        1000 ns
matching map         7.43 M      134.52 ns ±17446.18%           0 ns        1000 ns

Comparison:
checking size        7.51 M
matching map         7.43 M - 1.01x slower +1.40 ns

##### With input Large #####
Name                    ips        average  deviation         median         99th %
checking size        7.66 M      130.53 ns  ±3435.02%           0 ns        1000 ns
matching map         7.58 M      131.93 ns  ±7672.54%           0 ns        1000 ns

Comparison:
checking size        7.66 M
matching map         7.58 M - 1.01x slower +1.40 ns

##### With input Medium #####
Name                    ips        average  deviation         median         99th %
checking size        7.14 M      140.04 ns ±22155.54%           0 ns        1000 ns
matching map         7.07 M      141.45 ns ±21487.37%           0 ns        1000 ns

Comparison:
checking size        7.14 M
matching map         7.07 M - 1.01x slower +1.41 ns

##### With input Small #####
Name                    ips        average  deviation         median         99th %
matching map         7.78 M      128.49 ns  ±3673.23%           0 ns        1000 ns
checking size        7.74 M      129.22 ns  ±3346.12%           0 ns        1000 ns

Comparison:
matching map         7.78 M
checking size        7.74 M - 1.01x slower +0.74 ns

These calls are both so quick that I would occasionally receive this warning from benchee:

Warning: The function you are trying to benchmark is super fast, making measurements more unreliable! This holds especially true for memory measurements. See: https://github.com/PragTob/benchee/wiki/Benchee-Warnings#fast-execution-warning

I suspect that this explains why I would see the results flip-flop between runs.

I had expected map_size would be slower and slow down more as the input size increased, but the benchmarks show otherwise. If I had read the documentation for map_size, I could have saved myself some time.

This operation happens in constant time.

Since both options appear to be essentially equivalent, I plan to default to using map_size since it provides the flexibility to check for more than just empty maps.


Notice something wrong? Please consider proposing an edit or opening an issue.