Patterns and Specs
Overview
One of the most optimized parts of the BEAM VM are its pattern-matching capabilities. These can filter and map in-memory data structures very efficiently, and are used to power conditional expressions and multi-head functions.
When performance is essential, Erlang gives us a way to compile our own data-structure filter-map procedures into that low-level VM form: match patterns and specifications. They are much more performant¹ than invoking the same procedure as a pattern-matching function, skipping a lot of VM internals and overhead.
This is possible because they only support a limited set of safe and optimized pattern-matching operations, as well as key kernel functions, much like guards. They are expressed in a tuple-and-atom-based DSL resembling an erlang AST, that allows injection of literals and bound variables.
What is a match pattern?
A match pattern describes a data structure you can attempt to pattern-match against. You can think of it like one side of the input to the Kernel.SpecialForms.=/2
match operator, or the head of a 1-arity function.
# match operator
iex> {x, y, x} = {1, 2, 1}
{1, 2, 1}
iex> {x, y, x} = {1, 2, 3}
** (MatchError) no match of right hand side value: {1, 2, 3}
# function
iex> fun = fn {x, y, x} -> {x, y, x} end
iex> fun.({1, 2, 1})
{1, 2, 1}
iex> fun.({1, 2, 3})
** (FunctionClauseError) no function clause matching...
# match patterns
iex> pattern = Matcha.pattern {x, y, x}
#Matcha.Pattern<{:"$1", :"$2", :"$1"}>
iex> Matcha.Pattern.match!(pattern, {1, 2, 1})
{1, 2, 1}
iex> Matcha.Pattern.match!(pattern, {1, 2, 3})
** (MatchError) no match of right hand side value: {1, 2, 3}
They have limited uses compared to match specifications, but certain :ets
functions support them, so Matcha
does as well.
What is a match spec?
A match specification describes ways to transform a data structure if it matches certain criteria. You can think of it like the clauses of a Kernel.SpecialForms.case/2
statement, or a 1-arity function.
# case statement
iex> case {3, 4} do
...> {_x, 0} -> {:error, :division_by_zero}
...> {x, y} -> {:ok, x / y}
...> end
{:ok, 0.75}
iex> case {3, 0} do
...> {_x, 0} -> {:error, :division_by_zero}
...> {x, y} -> {:ok, x / y}
...> end
{:error, :division_by_zero}
iex> case {3, 2, 1} do
...> {_x, 0} -> {:error, :division_by_zero}
...> {x, y} -> {:ok, x / y}
...> end
** (CaseClauseError) no case clause matching: {3, 2, 1}
# function
iex> fun = fn
{_x, 0} -> {:error, :division_by_zero}
{x, y} -> {:ok, x / y}
end
iex> fun.({3, 4})
{:ok, 0.75}
iex> fun.({3, 0})
{:error, :division_by_zero}
iex> fun.({3, 2, 1})
** (FunctionClauseError) no function clause matching...
# match specifications
iex> spec = Matcha.spec do
{_x, 0} -> {:error, :division_by_zero}
{x, y} -> {:ok, x / y}
end
#Matcha.Spec<[
{{:"$1", 0}, [], [{{:error, :division_by_zero}}]},
{{:"$1", :"$2"}, [], [{{:ok, {:/, :"$1", :"$2"}}}]}
]>
iex> Matcha.Spec.match!(spec, {1, 2, 1})
:ok
iex> Matcha.Spec.match!(spec, {1, 2, 3})
** (MatchError) no match of right hand side value: {1, 2, 3}
spec = Matcha.spec do; {_x, 0} -> {:error, :division_by_zero}; {x, y} -> {:ok, x / y}; end
They may support special 'virtual' function calls² beyond guard-safe ones depending on context (ie :table
or :trace
usage). They can be validated at runtime, validated for special function utilization in specific contexts, and pre-compiled for performance optimization.
Footnotes
¹Per the Erlang Matchspec Docs:
The match specification in many ways works like a small function in Erlang, but is interpreted/compiled by the Erlang runtime system to something much more efficient than calling an Erlang function.
²
Per the Erlang Matchspec Docs, only match specifications for a :trace
context use special 'virtual' function calls (ActionCall
s).
The term 'virtual' is used here because none of these functions actually exist in erlang: unlike the rest of the matchspec-supported functions in the DSL, these calls have no concrete implementation that can be verified as correct by an erlang compiler.
Matcha
works around this by defining no-op implementations of these functions in a dedicated Matcha.Context.Trace
module, and referencing it during spec compilation.