Pattern Matching

technology

Pattern Matching

Definition

Pattern matching is a language feature that checks a value against a structural pattern and, if successful, destructures the value by binding its components to variables. In Elixir, pattern matching replaces most conditional logic with declarative multi-clause function definitions, case expressions, and the match operator (=). It is the primary control flow mechanism in the language, enabling concise, self-documenting code that handles different data shapes explicitly through separate function clauses rather than nested if/else chains.

Unlike equality checks or switch statements in imperative languages, pattern matching simultaneously performs three operations: it tests whether data conforms to an expected shape, it extracts components from the data by binding them to variables, and it selects the appropriate code path based on the match result. This unification of testing, extraction, and dispatch into a single syntactic construct makes Elixir code remarkably dense yet readable. A function with five clauses communicates five distinct cases and their handling in a format that is both machine-checkable and human-readable.

Pattern matching interacts deeply with Elixir's other core features. Immutability guarantees that matched data will not change during or after the match. The pipe operator chains functions that use pattern matching internally. Behaviours define callback signatures that are implemented through multi-clause pattern-matched functions. Ecto queries use pattern matching for result destructuring. Pattern matching is not merely a feature of Elixir -- it is the idiom through which all Elixir code is organized.

The Match Operator

The = operator in Elixir is the match operator, not an assignment operator. It attempts to match the left-hand side pattern against the right-hand side value:

# Simple binding (trivially matches any value)
x = 42

# Structure matching -- binds 'name' and 'role' from the map
%{name: name, role: role} = %{name: "Alice", role: :admin, id: 1}
# name = "Alice", role = :admin

# Tuple matching -- the standard Elixir result convention
{:ok, result} = {:ok, %{score: 850}}
# result = %{score: 850}

# Failure case -- MatchError raised when pattern doesn't match
# {:ok, result} = {:error, :not_found}
# ** (MatchError) no match of right hand side value: {:error, :not_found}

# Nested matching -- extracts deeply nested values
%{security: %{rating: %{grade: grade}}} = assessment
# grade bound to whatever value is at that path

The Pin Operator

The pin operator (^) forces a variable to be treated as its current value rather than a new binding. This is essential for matching against existing values:

expected_domain = "example.com"

# Without pin: 'domain' would be rebound to whatever value is in the map
%{domain: domain} = %{domain: "other.com"}
# domain = "other.com" (rebound)

# With pin: matches ONLY if domain equals "example.com"
%{domain: ^expected_domain} = %{domain: "example.com"}
# Succeeds -- value matches pinned variable

# %{domain: ^expected_domain} = %{domain: "other.com"}
# ** (MatchError) -- fails because "other.com" != "example.com"

Multi-Clause Function Definitions

Pattern matching's most powerful application is multi-clause function definitions, where different clauses handle different input shapes:

defmodule SecurityGrade do
  @spec assign(float()) :: atom()
  def assign(score) when score >= 800.0, do: :A
  def assign(score) when score >= 650.0, do: :B
  def assign(score) when score >= 500.0, do: :C
  def assign(score) when score >= 350.0, do: :D
  def assign(_score), do: :F
end

defmodule ResultHandler do
  # Each clause handles a different result shape
  def handle({:ok, %{data: data, meta: meta}}) do
    process_success(data, meta)
  end

  def handle({:ok, data}) when is_list(data) do
    process_batch(data)
  end

  def handle({:error, %{code: :not_found, resource: resource}}) do
    log_missing(resource)
    {:error, :not_found}
  end

  def handle({:error, %{code: :timeout}}) do
    {:retry, :exponential_backoff}
  end

  def handle({:error, reason}) do
    log_error(reason)
    {:error, reason}
  end
end

Guards

Guards extend pattern matching with additional constraints that go beyond structural shape. Guards appear in when clauses and support a restricted set of expressions that are guaranteed to be side-effect-free:

defmodule Validator do
  # Guards refine matches with type checks and comparisons
  def validate_score(score) when is_float(score) and score >= 300.0 and score <= 900.0 do
    {:ok, score}
  end

  def validate_score(score) when is_float(score) do
    {:error, :out_of_range}
  end

  def validate_score(_), do: {:error, :invalid_type}

  # Custom guard (defined with defguard for reuse)
  defguard is_valid_port(port) when is_integer(port) and port >= 1 and port <= 65535

  def validate_endpoint(host, port) when is_binary(host) and is_valid_port(port) do
    {:ok, {host, port}}
  end
end
Guard CategoryExamplesUse Case
Type Checksis_atom/1, is_binary/1, is_map/1, is_list/1Dispatch on type
Comparisons>, <, >=, <=, ==, !=Range validation
Booleanand, or, notCombining conditions
Arithmetic+, -, *, /, rem/2, abs/1Numeric constraints
Map/Tuplemap_size/1, tuple_size/1, elem/2Structure size checks
Stringbyte_size/1, bit_size/1Binary constraints
Customdefguard macrosDomain-specific guards

Pattern Matching in Case and With

Beyond function clauses, pattern matching drives case and with expressions:

# Case expression -- pattern matching on a single value
defmodule AssetClassifier do
  def classify(asset) do
    case asset do
      %{type: :domain, tld: tld} when tld in [".com", ".org", ".net"] ->
        {:standard_domain, asset}

      %{type: :domain} ->
        {:exotic_domain, asset}

      %{type: :ip, version: 4, address: addr} ->
        {:ipv4, addr}

      %{type: :ip, version: 6, address: addr} ->
        {:ipv6, addr}

      %{type: :certificate, expired: true} ->
        {:expired_cert, asset}

      _ ->
        {:unknown, asset}
    end
  end
end

# With expression -- chaining pattern-matched operations (happy path)
defmodule ScanPipeline do
  def execute(domain) do
    with {:ok, assets} <- discover_assets(domain),
         {:ok, rated_assets} <- rate_assets(assets),
         {:ok, report} <- generate_report(rated_assets),
         {:ok, _} <- store_report(report) do
      {:ok, report}
    else
      {:error, :dns_timeout} -> {:error, "DNS resolution timed out for #{domain}"}
      {:error, :no_assets} -> {:error, "No assets discovered for #{domain}"}
      {:error, reason} -> {:error, "Scan failed: #{inspect(reason)}"}
    end
  end
end

Context in Prismatic

Pattern matching is pervasive throughout the Prismatic Platform's 6,652 source files, serving as the primary mechanism for control flow, error handling, and data transformation across all 92 umbrella applications.

  • GenServer Callbacks: Multi-clause handle_call/3, handle_cast/2, and handle_info/2 definitions dispatch on message shape. Agent GenServers use pattern matching to route commands to appropriate handlers.
  • Result Convention: The universal {:ok, result} / {:error, reason} convention relies on pattern matching for error handling. Every function boundary in the platform uses this pattern.
  • Storage Adapters: Storage protocol implementations use pattern matching on adapter configuration and query parameters to dispatch to the correct backend (ETS, Ecto, Meilisearch, KuzuDB).
  • AIAD Agent Dispatch: Agent command routing uses pattern matching on agent specifications to direct commands to the correct handler module.
  • Quality Gate Logic: The 13 quality domain checkers use multi-clause functions to classify violations by severity, type, and remediation strategy.
  • Ecto Changeset Handling: Schema validation and changeset processing use pattern matching to handle valid and invalid states.
  • Phoenix Controllers: Request routing and parameter extraction rely on pattern matching in controller actions and plug pipelines.
  • Broadway Message Processing: Pipeline message handlers use pattern matching to dispatch on message type and content.

Pattern Matching vs. Alternatives

ApproachPattern Matching (Elixir)Switch/Case (Imperative)If/Else Chains
DispatchMulti-clause functionsSingle function with switchSingle function with if/else
ExtractionSimultaneous with matchManual after dispatchManual after condition
ExhaustivenessCompiler warnings for unmatchedNo compiler helpNo compiler help
Nested DataDeep destructuring in one expressionMultiple accessor callsMultiple accessor calls
ReadabilityDeclarative (what, not how)ProceduralProcedural
ExtensibilityAdd a clauseAdd a case branchAdd an else-if

Common Patterns

PatternExampleUse Case
Head/Tail[head | tail] = listRecursive list processing
Tagged Tuple{:ok, value} / {:error, reason}Result type convention
Struct Match%User{role: :admin} = userType-safe destructuring
Map Subset%{name: name} = big_mapExtract specific fields
Binary<<header::16, payload::binary>> = packetProtocol parsing
String Prefix"https://" <> rest = urlString pattern extraction
Ignore{:ok, _meta, data}Discard unneeded values
  • Pipe Operator - Composition operator complementing pattern-matched functions
  • Pure Function - Multi-clause pure functions enabled by pattern matching
  • Immutability - Immutable data structures matched without defensive copying
  • Behaviour - OTP callback contracts implemented via pattern-matched clauses
  • Ecto - Database library using pattern matching for query results and changesets
  • Typespec - Type specifications documenting pattern-matched function signatures
  • Dialyzer - Static analysis that verifies pattern match exhaustiveness
  • GenStage - Pipeline stages dispatching on message patterns
  • Property-Based Testing - Testing that generates inputs for pattern-matched functions
  • Formal Verification - Verification of pattern match completeness and correctness
  • Match Specification

See Also

  • Technologies - Elixir language features and idioms
  • Architecture - Pattern matching in platform design patterns
  • Capabilities - Platform capabilities leveraging declarative dispatch

Connect & Contribute

Created by Tomáš Korcak (korczis) | Open Source under GHL