← Code Compare

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.

Show: ErlangElixirGleamLFELuerl
Erlang
-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.

Elixir
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.

Gleam
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.

LFE
(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).

Luerl
-- 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.