Find Patterns and Guards

Published by Programming Line in Ruby. Last updated .

A find pattern lets you search an array for a subsequence that matches a shape, no matter where it sits, instead of insisting the array line up element-for-element from the start. Paired with a guard, an if or unless tacked onto an in clause, you can add an extra boolean condition that must also hold before a branch is accepted. Together they turn Ruby’s case/in pattern matching into a genuinely useful tool for scanning collections of real-world data, such as log lines, parsed tokens, or API responses, for the interesting bit buried inside.

Overview: How Find Patterns Work

Ruby’s pattern matching (case/in) offers several pattern shapes. An array pattern like [a, b, c] requires the array to have exactly three elements matching in order. A splat at one end, like [first, *rest] or [*init, last], relaxes the length but still anchors the fixed part to one edge of the array. A find pattern goes further: it is written with a splat on both sides, [*, middle, *], and it means “somewhere in this array there exists a run of elements matching middle, with anything (including nothing) before and after it.”

Under the hood, Ruby implements a find pattern by trying successive positions for the fixed middle section, starting from the left, until the elements at that position structurally match every fixed sub-pattern in the middle. As soon as a position matches, Ruby binds the two splats (if named) to the elements before and after the match, binds any variables in the middle patterns, and the pattern as a whole succeeds. If no position matches anywhere in the array, the pattern fails and Ruby moves on to the next in clause, or to else, or raises NoMatchingPatternError if there is neither.

A guard is a plain boolean expression attached to an in clause with if or unless, evaluated after the pattern has already matched structurally and bound its variables. It lets you layer an ordinary Ruby condition, using any variables the pattern just bound, on top of the structural match. Crucially, the guard is checked exactly once, against the single position the find pattern already settled on; if the guard is false, the whole in clause fails, and Ruby does not go back and try a different position in the array. This distinction matters a lot in practice and is covered in Common Mistakes below.

Syntax

The general shape of a find pattern with an optional guard:

case value
in [*, middle_pattern, *]
  # matched, no extra condition
in [*pre, middle_pattern, *post] if condition
  # matched, and condition is true; pre/post capture the surrounding elements
in [*, middle_pattern, *] unless condition
  # matched, and condition is false
else
  # nothing matched
end
Part Meaning
* (anonymous) Matches zero or more elements on that side; the elements themselves are discarded.
*pre / *post A named splat, binds the elements before/after the match to an array, so you can inspect the surrounding context.
middle_pattern One or more fixed sub-patterns (literals, classes, Pattern => name bindings, nested array/hash patterns) that must appear consecutively at the matched position.
if condition A guard, an ordinary Ruby expression evaluated once after the structural match succeeds. Must be truthy for the branch to run.
unless condition The inverse guard, the branch runs only if condition is falsy (nil or false).

Examples

Example 1: Locating a value anywhere in an array

data = [1, 2, 3, 42, 5, 6]

case data
in [*, 42, *]
  puts "Found 42 in the array"
else
  puts "Not found"
end

Output:

Found 42 in the array

The pattern [*, 42, *] does not care where 42 sits. Ruby scans from the left: the element at index 3 is 42, so the fixed middle matches there, the two anonymous splats absorb everything before and after, and the branch runs.

Example 2: Finding a matching hash and capturing it with a guard

logs = [
  { level: :info, code: 200 },
  { level: :info, code: 200 },
  { level: :error, code: 503 },
  { level: :info, code: 200 }
]

case logs
in [*, { level: :error, code: Integer => code } => bad_entry, *] if code >= 500
  puts "Server error found: level=#{bad_entry[:level]}, code=#{bad_entry[:code]}"
else
  puts "No server error"
end

Output:

Server error found: level=error, code=503

The middle pattern is a hash pattern requiring level: :error and an integer code. Only the third entry matches structurally, so Ruby binds code to 503 and bad_entry to the whole hash, then checks the guard code >= 500, which is true. Note the => binding inside the pattern (Integer => code) and the separate => binding around the whole hash ({ ... } => bad_entry); both are legal in the same pattern.

Example 3: Matching a run of several elements, with surrounding context

tokens = ["the", "sky", "is", "not", "blue", "today"]

case tokens
in [*before, "not", String => word, *after] if word != "very"
  puts "Negation of '#{word}' found"
  puts "Words before: #{before.join(' ')}"
  puts "Words after: #{after.join(' ')}"
else
  puts "No simple negation found"
end

Output:

Negation of 'blue' found
Words before: the sky is
Words after: today

This find pattern’s middle section has two fixed elements: the literal string "not" followed by any String. Ruby finds the first position where both line up consecutively, at index 3 and 4, binds word to "blue", and binds the named splats before and after to the arrays of elements on either side. The guard then runs once against that single match.

How It Works Step by Step

  1. Ruby evaluates the expression after case once, producing the value to match against (here, always an array for a find pattern).
  2. For the first in clause, Ruby checks whether the value can be treated as an array (arrays already can; custom objects can implement #deconstruct).
  3. Ruby then tries the find pattern’s middle sub-patterns starting at index 0: do the elements starting here match every fixed pattern in the middle, in order? If not, it tries index 1, then index 2, and so on, up to the last position where the middle could still fit.
  4. The first position that matches wins. Ruby binds any named splats to the slices before and after that position, and binds any Pattern => name captures inside the middle.
  5. If the clause has a guard, Ruby evaluates it now, with all the bindings from step 4 in scope. A truthy result (for if) or falsy result (for unless) means the clause matches and its body runs; otherwise the clause fails.
  6. If the clause fails at any step, either no position matches structurally, or the guard rejects it, Ruby moves to the next in clause and repeats from step 3, or falls through to else if there is one, or raises NoMatchingPatternError if there is neither.

Common Mistakes

Mistake 1: Expecting a guard to keep searching for a later match

A guard only ever tests the single position the find pattern already locked onto, it does not make Ruby backtrack and try a different subsequence.

readings = [12, 87, 15]

case readings
in [*, Integer => spike, *] if spike > 50
  puts "Spike: #{spike}"
else
  puts "No spike"
end

Output:

No spike

Every element is an Integer, so the find pattern matches structurally at the very first position (index 0, spike = 12) and stops looking, it never even considers 87. The guard then checks 12 > 50, which is false, so the whole clause fails and control falls to else, even though a real spike exists later in the array. Find patterns pick the leftmost structural match and commit to it; a guard can only accept or reject that one match, not go looking for another.

To search among several candidates, filter first and match on the result:

readings = [12, 87, 15]
spike = readings.find { |n| n > 50 }

case spike
in Integer => value
  puts "Spike: #{value}"
else
  puts "No spike"
end

Output:

Spike: 87

Mistake 2: Writing an array pattern when you meant a find pattern

A splat on only one side anchors the match to that edge of the array, it is not the same as searching anywhere.

data = [42, 1, 2]

case data
in [*, 42]
  puts "Found 42"
else
  puts "Not found"
end

Output:

Not found

[*, 42] is an array pattern that requires 42 to be the last element, the leading splat only absorbs everything before it. Here the last element is 2, so the match fails, even though 42 is clearly in the array. A genuine find pattern needs a splat on both sides:

data = [42, 1, 2]

case data
in [*, 42, *]
  puts "Found 42"
else
  puts "Not found"
end

Output:

Found 42

Best Practices

  • Reach for a find pattern only when the position of the match genuinely doesn’t matter; if you know the value must be first, last, or at a fixed index, a plain array pattern is clearer and cheaper.
  • Name the splats (*pre, *post) whenever you need the surrounding context; use the anonymous * when you only care whether a match exists.
  • Remember that a guard is checked once, against the leftmost structural match, never rely on it to skip a match and keep searching for a better one further in the array.
  • Prefer Pattern => name bindings over separate lookups afterward; they keep the captured value tied directly to the pattern that produced it.
  • Always provide an else branch (or handle NoMatchingPatternError) since an unmatched case/in with no fallback raises.
  • Keep guard expressions simple and free of side effects; they run as part of logic a reader expects to be purely structural.

Practice Exercises

  • Given tokens = [:a, "x", :b, "y", :c], write a find pattern that detects a String immediately followed by a Symbol, and prints the pair. Hint: your middle section needs two fixed sub-patterns.
  • Given an array of user hashes like { name: "Ana", age: 17 }, write a case/in with a find pattern and a guard that prints the name of the first user whose age is 18 or over.
  • Predict, then verify by tracing through by hand: for [5, 5, 99, 5], does in [*, Integer => n, *] if n == 99 match? Explain why, based on how the leftmost structural match interacts with the guard.

Summary

  • A find pattern is written [*, middle, *], splats on both sides let it match a subsequence anywhere in an array.
  • Ruby searches for the leftmost position where the middle sub-patterns match consecutively, then commits to it.
  • Named splats (*pre, *post) capture the elements surrounding the match; anonymous * discards them.
  • A guard (if/unless) adds a plain boolean condition, evaluated once after the structural match; it filters that one match, it does not trigger a further search.
  • A splat on only one side is an array pattern anchored to that edge, not a find pattern, don’t confuse the two.
  • Always give case/in an else, since an unmatched pattern with no fallback raises NoMatchingPatternError.