In Ruby case/in, how do the find pattern, | alternatives and if/unless guards work, and which of them may bind variables?
answer
- [*, x, *] searches inside
- stable since 3.2
- | cannot capture, except _names
- guard runs after binding
- guards only on case/in
basics
~20 sA 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 sThe **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 linescutoff = 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) # => :messagego deeper
Recognise [*, x, *] as searching inside an array, | as either-or, and in pattern if cond as a guard on a case/in branch.
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.
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.
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.