Internal
Published because the matching rules below are the exact semantics of every template in
the library, and there is nowhere better to state them. It is not part of the public
API and not covered by semantic versioning. Use Tuplex.
Validation, shard-key extraction, and match-spec compilation for tuples and templates.
This module is pure. It performs no side effects, owns no state, and never touches
:ets — it only decides what a template means. Tuplex.Store turns those decisions
into table operations, and owns the storage form; nothing here knows how a tuple is
stored.
Tuples and templates
A tuple is what you write with Tuplex.out/1. A template is what you match with
(in, rd, inp, rdp, rd_all, watch). Both are Erlang tuples whose first element
is an atom tag:
{:job, 1, "payload"} # a tuple
{:job, :_, :_} # a template matching any 3-arity :job tupleMatching is exact
Matching uses strict equality, which is the correct Linda semantics: the float 1.0 does
not match the stored integer 1. Only :_ is a wildcard, and it matches at any depth:
matches?({:point, :_, 2}, {:point, 1, 2}) #=> true
matches?({:point, {:x, :_}}, {:point, {:x, 9}}) #=> true
matches?({:n, 1.0}, {:n, 1}) #=> falseArity and tag are part of the match, so an arity-3 template never matches an arity-2
tuple and a :job template never matches a :task tuple.
Maps
Maps are ordinary Elixir terms and templates carry them fine, but they are never placed
in the ETS head pattern. ETS matches a map in a head partially — a %{a: 1} pattern
matches a stored %{a: 1, b: 2} — which would be a second, contradictory notion of
"match" living alongside the exact one above.
So compile/1 hoists any map-bearing subterm out of the head and re-attaches it as an
=:= guard:
compile({:job, :_, %{region: :north}})
#=> {{:job, :_, :"$1"}, [{:"=:=", :"$1", {:const, %{region: :north}}}]}=:= is exactly the equality the rest of the module promises: the map must be equal, not
a subset, and 1.0 still does not match 1. The hoist takes the largest wildcard-free
subterm containing the map, so one guard usually covers a whole nested structure, and
wildcards elsewhere in the template keep working normally.
The one thing this cannot express is a wildcard inside a map — %{a: :_}. That would
need the partial semantics back, so it is rejected. Match the enclosing position with
:_ and filter the results yourself.
What a template may not contain
A wildcard tag.
{:_, :_}is rejected: the tag selects the shard, so a wildcard tag would mean fanning every read across every shard. UseTuplex.tags/0and fold over it if you genuinely want that — then the cost is visible at the call site.:"$1"-style atoms in head positions. ETS reads any atom beginning with$as a match variable, so{:x, :"$1"}would quietly behave as a second wildcard. They are fine inside a hoisted subterm, where they end up in a{:const, _}guard and are never interpreted —{:cfg, %{name: :"$1"}}is accepted.Wildcards inside maps, as above.
None of these apply to tuples passed to out/1. Stored tuples are data: they are never
interpreted as patterns, so they may contain :_, :"$1", and maps freely. Only the tag
rule is shared, because storage still has to route them.
Summary
Types
Why a tuple or template was rejected.
An ETS guard pinning a hoisted subterm to an exact value.
An ETS head pattern, shaped like the template it came from.
The waiter-index key: the tuple's tag paired with its arity.
A tuple written into the space with Tuplex.out/1.
A pattern matched against stored tuples. :_ is the wildcard.
Functions
Compiles a template into an ETS head pattern and its guards.
Same as compile/1 but returns the pair or raises ArgumentError.
Returns the waiter-index key for a tuple or template: its tag paired with its arity.
Returns whether tuple matches template, without consulting ETS.
Validates a template, returning it unchanged.
Same as validate/1 but returns the template or raises ArgumentError.
Validates a tuple destined for the space.
Same as validate_tuple/1 but returns the tuple or raises ArgumentError.
Types
@type error() :: :not_a_tuple | :empty_tuple | :wildcard_tag | {:non_atom_tag, term()} | {:reserved_atom, atom()} | {:wildcard_in_map, map()}
Why a tuple or template was rejected.
An ETS guard pinning a hoisted subterm to an exact value.
@type head() :: tuple()
An ETS head pattern, shaped like the template it came from.
@type key() :: {atom(), non_neg_integer()}
The waiter-index key: the tuple's tag paired with its arity.
@type t() :: tuple()
A tuple written into the space with Tuplex.out/1.
@type template() :: tuple()
A pattern matched against stored tuples. :_ is the wildcard.
Functions
Compiles a template into an ETS head pattern and its guards.
The head is shaped exactly like the template, so tag and arity are matched structurally
and cheaply. Map-bearing subterms are replaced by :"$1", :"$2", … and pinned by
=:= guards; a template with no maps compiles to itself and no guards at all.
The caller composes the final match spec, because the record layout belongs to
Tuplex.Store, not here. Store nests this head under its own key position, so the
variables numbered here can never collide with anything Store introduces.
Examples
iex> Tuplex.Template.compile({:job, :_, 2})
{:ok, {{:job, :_, 2}, []}}
iex> Tuplex.Template.compile({:job, :_, %{region: :north}})
{:ok, {{:job, :_, :"$1"}, [{:"=:=", :"$1", {:const, %{region: :north}}}]}}
iex> Tuplex.Template.compile({:job, %{a: :_}})
{:error, {:wildcard_in_map, %{a: :_}}}
Same as compile/1 but returns the pair or raises ArgumentError.
Returns the waiter-index key for a tuple or template: its tag paired with its arity.
A tuple and every template that can match it share this key. The shard relies on
that: a newly written tuple only has to be offered to waiters filed under its own key,
and if the two ever disagreed a legitimately waiting in would never wake.
Examples
iex> Tuplex.Template.key({:job, 1, "payload"})
{:job, 3}
iex> Tuplex.Template.key({:job, :_, :_})
{:job, 3}
iex> Tuplex.Template.key({:ping})
{:ping, 1}
Returns whether tuple matches template, without consulting ETS.
This is the same relation the compiled match spec expresses, implemented in plain Elixir so a shard can test a freshly written tuple against waiting templates without a table round-trip.
Examples
iex> Tuplex.Template.matches?({:job, :_, 2}, {:job, 1, 2})
true
iex> Tuplex.Template.matches?({:job, :_}, {:job, 1, 2})
false
iex> Tuplex.Template.matches?({:n, 1.0}, {:n, 1})
false
iex> Tuplex.Template.matches?({:cfg, %{a: 1}}, {:cfg, %{a: 1, b: 2}})
false
Validates a template, returning it unchanged.
Equivalent to compile/1 with the compiled form discarded — a template is valid exactly
when it compiles.
Examples
iex> Tuplex.Template.validate({:job, :_, 2})
{:ok, {:job, :_, 2}}
iex> Tuplex.Template.validate({:job, %{region: :north}})
{:ok, {:job, %{region: :north}}}
iex> Tuplex.Template.validate({:_, 1})
{:error, :wildcard_tag}
iex> Tuplex.Template.validate({:job, :"$1"})
{:error, {:reserved_atom, :"$1"}}
iex> Tuplex.Template.validate([:job])
{:error, :not_a_tuple}
Same as validate/1 but returns the template or raises ArgumentError.
Validates a tuple destined for the space.
Only the tag is constrained — it must be a concrete atom, because it selects the shard. Everything else is data and is stored verbatim, including terms that would be illegal in a template.
Examples
iex> Tuplex.Template.validate_tuple({:job, 1, "payload"})
{:ok, {:job, 1, "payload"}}
iex> Tuplex.Template.validate_tuple({:cfg, %{retries: 3}, :"$1"})
{:ok, {:cfg, %{retries: 3}, :"$1"}}
iex> Tuplex.Template.validate_tuple({})
{:error, :empty_tuple}
iex> Tuplex.Template.validate_tuple({"job", 1})
{:error, {:non_atom_tag, "job"}}
Same as validate_tuple/1 but returns the tuple or raises ArgumentError.