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:
{:_, %{number: :"$1"}}
Look in all keys for an object where there's a map with anumber
key and bind its value to:"$1"
[{:"==", :"$1", 901}]
Guard where:"$1"
is equal to901
[:"$_"]
Return everything that matches (key and object)
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
match/2
andmatch_object/2
support simple match patternsselect/2
supports more complex matches based on Erlang Match Specification- Erlang's Efficiency Guide recommends using
select/2
instead ofmatch/2
andmatch_object/2
for performance reasons
If you had issues following this post, check out these materials:
- Elixir School ETS lesson
- Elixir Getting Started on ETS