skip to content

In Ruby case/in, how do the find pattern, | alternatives and if/unless guards work, and which of them may bind variables?

level: seniorimportance: nice to knowfreq 24%

answer

  1. [*, x, *] searches inside
  2. stable since 3.2
  3. | cannot capture, except _names
  4. guard runs after binding
  5. guards only on case/in

basics

~20 s

A find pattern [*, p, *] matches a run of elements anywhere in an array and can bind them. Alternatives p1 | p2 accept either shape but cannot bind (except _names). Guards (in p if cond) run after binding, only on case/in branches.

solid answer

~50 s

The **find pattern** `[*pre, pattern, *post]` searches an array for a consecutive run of elements matching the middle sub-patterns, binding them and optionally the elements before and after; it was experimental in 3.0 and has been stable since 3.2. **Alternatives** `p1 | p2` accept any of several patterns — `in {type: "message" | "message_changed"}` — but a variable capture inside an alternative is a `SyntaxError` ("variable capture in alternative pattern"), because some branches would leave the variable unset; only names starting with `_` are allowed. **Guards** attach a boolean after the pattern: `in {type: "message", ts:} if ts > cutoff`, or `unless`. The guard runs after the pattern matched and bound its variables; if it fails, matching continues with the next branch. Guards exist only on `case/in` branches — after a one-line `in` or `=>`, a trailing `if` is a statement modifier.

code

ruby · 20 lines
ruby
cutoff = 1_700_000_000

def classify(event, cutoff)
  case event
  in {type: "message" | "message_changed", ts: Integer => ts} if ts < cutoff
    :stale
  in {blocks: [*, {type: "mention", user:}, *]}
    "mention of #{user}"
  in {type: "message" | "message_changed"}
    :message
  else
    :other
  end
end

p classify({type: "message", ts: 1}, cutoff)             # => :stale
p classify({type: "message", ts: cutoff + 5,
            blocks: [{type: "text"}, {type: "mention", user: "U7"}]}, cutoff)
                                                         # => "mention of U7"
p classify({type: "message_changed", ts: cutoff + 5}, cutoff)  # => :message

go deeper

for a junior

Recognise [*, x, *] as searching inside an array, | as either-or, and in pattern if cond as a guard on a case/in branch.

for a middle

Explain why captures are forbidden inside alternatives, that guards run after binding and fall through on failure, and that one-line forms take no guard.

for a senior

Order branches so catch-alls and guarded patterns do not hide others, use find patterns for nested payload blocks, and lean on the pending Lint cops for unreachable or duplicate branches.

for a principal

Judge when a dense case/in with alternatives and guards has become a rules engine that deserves its own tested abstraction rather than one growing method.

## Find patterns: searching inside an array An ordinary array pattern must match the whole array. A **find pattern** instead looks for a matching **run** of elements anywhere inside it: ```ruby case blocks in [*, {type: "mention", user:}, *] notify(user) end ``` - The leading and trailing splats are required and may be bare `*` or named (`*before`, `*after`), which then bind the elements on either side. - The middle can be several sub-patterns; they must match **consecutive** elements. - The first position where the middle fits is used. The reference's example: `["a", 1, "b", "c", 2]` matches `[*, String, String, *]` because `"b", "c"` are adjacent Strings. History: the find pattern was added as experimental in Ruby 3.0 and declared stable in 3.2. ## Alternatives: one of several shapes `|` combines patterns; the first alternative that matches wins: ```ruby in {type: "message" | "message_changed", text:} in Integer | Float in [:ok, _] | [:accepted, _] ``` The restriction is on **binding**. A capture inside an alternative is rejected by the parser with "variable capture in alternative pattern": ```ruby in {text:} | {body:} # SyntaxError ``` The reason is soundness: if the first alternative matched, `body` would be unset, and vice versa. The only exception is names beginning with an underscore, which signal a discarded value; the reference allows `in {a: _, b: _foo} | Array` but advises against reusing such values. Note that `{type: "message" | "message_changed", text:}` is fine: the alternative is only the value of `type`, and `text:` binds outside it. ## Guards: conditions after the pattern A guard is `if` or `unless` after an `in` pattern: ```ruby case event in {type: "message", ts: Integer => ts} if ts >= cutoff handle(event) in {type: "message"} :stale end ``` Mechanics: 1. The pattern is matched first and its variables are bound. 2. The guard expression is evaluated and may use those variables. 3. If the guard is falsy (or, with `unless`, truthy), the branch is skipped and matching continues with the next `in`. Guards appear **only on `case/in` branches**. The reference is explicit that `=>` and `in` cannot have a guard, and that `[1, 2] in a, b if b == a*2` is parsed as a standalone expression with a modifier `if`. ## Choosing between them | Need | Tool | Binds? | |---|---|---| | "Some element in this array looks like X" | find pattern `[*, X, *]` | yes | | "The value is one of these shapes/values" | alternative `A \| B` | only `_`-prefixed names | | "The shape fits and a condition on its parts holds" | guard `in P if cond` | variables from `P` are usable | | "Two parts are equal" | pin `^name` inside the pattern | yes | ## Traps to avoid - A catch-all such as `in _` or `in x` before more specific branches makes them unreachable. RuboCop's `Lint/UnreachablePatternBranch` (pending, added in 1.85) flags it, and it treats `in _ if cond` as not a catch-all because the guard may fail. - Repeated identical patterns across branches are flagged by `Lint/DuplicateMatchPattern` (pending). - An `in` branch with no body is flagged by `Lint/EmptyInPattern` (pending). - Guards that call expensive methods run for every candidate that reaches them; order branches so cheap structural checks filter first. ## Summary The find pattern searches, alternatives choose, and guards refine. Only the find pattern and ordinary sub-patterns bind freely; alternatives cannot capture (bar `_names`); guards can read what the pattern bound, but exist only on `case/in` branches.

  • Why is `in {text:} | {body:}` a SyntaxError?
    Captures inside an alternative are forbidden, because whichever alternative matched, the other's variable would be unset. The parser reports "variable capture in alternative pattern". Split it into two `in` branches, or bind outside the alternative. Only names starting with `_` are exempt, since they mark discarded values.
  • What happens when a case/in branch's pattern matches but its guard is false?
    The branch is skipped and matching continues with the next `in` clause; if none match and there is no `else`, `NoMatchingPatternError` is raised. Variables bound by the skipped pattern should not be relied on afterwards.

saying these in an interview costs you the question

  • A find pattern's middle sub-patterns may match elements that are not adjacent.
  • You can bind a different variable in each branch of an | alternative.
  • A guard is evaluated before the pattern, so it cannot use pattern variables.
  • expr in pattern if cond applies the guard to the one-line match.
  • The find pattern is still experimental in Ruby 4.0.