ETS / DETS match patterns and specifications

You may have used ETS or DETS in the past. They share an almost identical API and today I'm reminding myself (and you, maybe) that we need to carefully read the docs.

ETS will be used for snippets but everything applies to DETS the same way here. Also, set, bag and duplicate_bag types behave the same.

So, imagine we have the following table:

iex> table = :ets.new(:address_book, [:set, :protected, :named_table])
:address_book

iex> :ets.insert(table, {"55-123456789", %{name: "Yuri", address: "Newbie Street", number: 101, country_code: 55}})
true

iex> :ets.insert(table, {"55-987654321", %{name: "Meru", address: "Mountain Ave", number: 901, country_code: 55}})
true

iex> :ets.insert(table, {"55-223456789", %{name: "Satya", address: "Newbie Street", country_code: 55}})            
true

And we want to search for objects where number keys are equal to 901. Let's search for any key on this example.

match/2

:ets.match/2 requires a match_pattern() as a second argument. That is, an atom() or a tuple(). Let's try that:

iex> :ets.match table, {:_, %{number: 901}}
[[]]

WAT?

Well, we need at least a pattern variable (in Erlang terms) to match something.

iex> :ets.match table, {:_, %{number: :"$1"}}
[[901], 'e']

Ok, now we have all bindings we wanted to, with the downside of having to filter for 901 in our application code. We can't add any guards or extra logic here.

match_object/2

:ets.match_object/2 also requires a match_pattern() to be present in order to return something. Rather than returning only what we bind to match, match_object returns the entire object and the key. Let's check this out:

iex> :ets.match_object table, {:_, %{number: 901}}
[
  {"55-987654321",
   %{address: "Mountain Ave", country_code: 55, name: "Meru", number: 901}}
]

Okay, so now we can match exactly on what we want!

Also note that if we try binding the same way we did with match/2, we'd get all objects with a map and a number key.

iex> :ets.match_object table, {:_, %{number: :"$1"}}                                                   
[
  {"55-987654321",
   %{address: "Mountain Ave", country_code: 55, name: "Meru", number: 901}},
  {"55-123456789",
   %{address: "Newbie Street", country_code: 55, name: "Yuri", number: 101}}
]

select/2

:ets.select/2 accepts a more complex second argument, a match_spec(). Match specification is a super set of a match pattern we saw on both match/2 and match_object/2, accepting guards and a MatchBody, which defines how you want your matches to return.

Roughly, match_spec is [{ match_tuple(), [guard_tuple()], [body] }].

iex> :ets.select table, {:_, %{number: :"$1"}}
** (ArgumentError) errors were found at the given arguments:

  * 2nd argument: not a valid match specification

    (stdlib 4.0.1) :ets.select(:address_book, {:_, %{number: :"$1"}})

So we can't pattern match in select/2, but we can have a more complex and refined pattern (a match spec):

iex> :ets.select table, [{{:_, %{number: :"$1"}}, [{:"==", :"$1", 901}], [:"$_"]}]
[
  {"55-987654321",
   %{address: "Mountain Ave", country_code: 55, name: "Meru", number: 901}}
]

This specification [{{:_, %{number: :"$1"}}, [{:"==", :"$1", 901}], [:"$_"]}] means:

What if

Instead of looking for number keys to be equal to 901 we wanted objects where number keys are greater than 100?

Well, both match/2 and match_object/2 functions wouldn't be enough. We'd have to return all objects with number keys and filter the ones we want in out application code.

Also, match/2 and match_object/2 don't support Match Specification arguments, only simpler patterns.

But with select/2 that's fairly easy:

iex> :ets.select table, [{{:_, %{number: :"$1"}}, [{:">", :"$1", 100}], [:"$_"]}]                      
[
  {"55-987654321",
   %{address: "Mountain Ave", country_code: 55, name: "Meru", number: 901}},
  {"55-123456789",
   %{address: "Newbie Street", country_code: 55, name: "Yuri", number: 101}}
]

If you have even more complex match cases to try with select/2, the :ets.fun2ms/1 would be the way to go in order to get a more readable match specification.

Conclusions

If you had issues following this post, check out these materials:

#elixir #otp