Pattern Matching
Pattern matching is the beating heart of BEAM programming: instead of testing and indexing, you describe the shape of data and let the runtime bind the pieces. Watch the same task - destructuring an {ok, Value}/{error, Reason} tuple and the list shapes [], [x], and [head|tail] - expressed as multi-clause functions, case expressions, and assignments. Notice how Erlang, Elixir, and LFE share the same dynamic match semantics, Gleam adds compiler-enforced exhaustiveness, and Luerl (Lua) has to fake it all by hand.
-module(matching).
-export([handle/1, describe/1, demo/0]).
%% Match on the {ok, Value} / {error, Reason} tagged-tuple convention.
handle({ok, Value}) ->
io:format("got ~p~n", [Value]),
Value;
handle({error, Reason}) ->
io:format("failed: ~p~n", [Reason]),
Reason.
%% Match on list shapes: empty, single element, head|tail.
describe([]) -> empty;
describe([X]) -> {one, X};
describe([H | T]) -> {cons, H, T}.
%% Use it: pattern matching also works on the left of =.
demo() ->
{ok, N} = {ok, 42},
empty = describe([]),
{cons, 1, [2, 3]} = describe([1, 2, 3]),
N.Each function clause is a pattern; Erlang picks the first head that matches and binds the variables. The same matching works on the left of =, where a failed match crashes loudly by design.
defmodule Matching do
# Multi-clause functions match the {:ok, value} / {:error, reason} convention.
def handle({:ok, value}), do: IO.puts("got #{inspect(value)}")
def handle({:error, reason}), do: IO.puts("failed: #{inspect(reason)}")
# Match on list shapes with clause heads.
def describe([]), do: :empty
def describe([x]), do: {:one, x}
def describe([head | tail]), do: {:cons, head, tail}
end
# case is the same idea inline; with/<- threads happy-path matches.
result = {:ok, 42}
case result do
{:ok, value} -> "value is #{value}"
{:error, reason} -> "oops: #{reason}"
end
{:one, 7} = Matching.describe([7])Elixir uses the same multi-clause heads as Erlang plus inline case; {:ok, value} <- style matching in with keeps the happy path flat while letting failures fall through.
import gleam/io
import gleam/int
pub type Outcome {
Ok(Int)
Err(String)
}
// case must be exhaustive: the compiler checks every variant is covered.
pub fn handle(outcome: Outcome) -> String {
case outcome {
Ok(value) -> "got " <> int.to_string(value)
Err(reason) -> "failed: " <> reason
}
}
// Match list shapes: empty, single, and head/tail with [first, ..rest].
pub fn describe(items: List(Int)) -> String {
case items {
[] -> "empty"
[x] -> "one: " <> int.to_string(x)
[first, ..rest] ->
"cons " <> int.to_string(first) <> " + " <> describe(rest)
}
}
pub fn main() {
io.println(handle(Ok(42)))
io.println(describe([1, 2, 3]))
}Gleam's case is checked for exhaustiveness at compile time, so forgetting the [] or Err branch is a type error - there is no fall-through and no runtime case_clause surprise.
(defmodule matching
(export (handle 1) (describe 1)))
;; Pattern-match the (tuple 'ok value) / (tuple 'error reason) convention.
(defun handle
(((tuple 'ok value))
(io:format "got ~p~n" (list value))
value)
(((tuple 'error reason))
(io:format "failed: ~p~n" (list reason))
reason))
;; Match list shapes with cons patterns in clause heads.
(defun describe
(('()) 'empty)
(((list x)) (tuple 'one x))
(((cons h t)) (tuple 'cons h t)))
;; case does the same inline; matching also binds in let.
(let (((tuple 'ok n) (tuple 'ok 42)))
(case (describe (list 1 2 3))
((tuple 'cons head tail) (list head tail))
('empty 'nothing)))Lisp Flavoured Erlang keeps Erlang's pattern matching but in S-expressions: clause heads are (pattern ...) lists and tuples/conses are written as (tuple ...) and (cons h t).
-- Lua has no built-in pattern matching, so the BEAM idiom is emulated:
-- return a {tag, value} table and switch on the tag field.
local function handle(result)
if result.tag == "ok" then
print("got", result.value)
return result.value
elseif result.tag == "error" then
print("failed:", result.reason)
return result.reason
end
end
-- "List shapes" via the length operator and array indexing.
local function describe(list)
local n = #list
if n == 0 then
return "empty"
elseif n == 1 then
return "one: " .. tostring(list[1])
else
local head = list[1]
local tail = { table.unpack(list, 2) } -- head|tail, manually
return "cons " .. tostring(head) .. " + " .. describe(tail)
end
end
handle({ tag = "ok", value = 42 })
print(describe({ 1, 2, 3 }))Lua has no pattern matching, so the BEAM idiom is simulated with tagged tables plus if/elseif on a tag field, and list shapes are recovered from the # length operator and table.unpack.