# Pattern Matching in Ruby

Pattern matching is one of the most transformative features added to Ruby in recent versions. Instead of writing nested conditionals and type checks, pattern matching lets you destructure data and match against specific shapes in a single expression. This declarative approach makes your code more readable and reduces the cognitive overhead of understanding complex data flows.

Ruby's pattern matching was introduced in Ruby 2.7 as an experimental feature and became stable in Ruby 3.0. Unlike traditional case statements that only match values, pattern matching can match against data structure shapes, extract nested values, and apply guards all in one expression. The feature draws inspiration from functional languages while maintaining Ruby's characteristic readability.

In this tutorial, you'll learn:

- How pattern matching differs from traditional case statements
- How to match against arrays, hashes, and objects
- How to extract and bind values during matching
- How to use guards and alternative patterns effectively


[ad-logs]

## Prerequisites

This guide assumes you have Ruby 3.0 or newer installed on your system. Pattern matching works in Ruby 2.7, but some syntax improvements and features require Ruby 3.0+. You should understand basic Ruby syntax, including case statements, arrays, and hashes.

## Understanding pattern matching fundamentals

Pattern matching in Ruby uses the `case`/`in` syntax instead of the traditional `case`/`when` structure. The key difference is that `in` clauses match against patterns rather than just comparing values:

```command
mkdir ruby-patterns && cd ruby-patterns
```

```ruby
[label app.rb]
# Traditional case statement
value = 5
result = case value
when 1, 2, 3
  "small"
when 4, 5, 6
  "medium"
else
  "large"
end
puts result

# Pattern matching
result = case value
in 1 | 2 | 3
  "small"
in 4 | 5 | 6
  "medium"
else
  "large"
end
puts result
```

The first case statement shows Ruby's traditional matching behavior, which compares values using the `===` operator. This works well for simple value matching but becomes cumbersome when you need to inspect data structures. The pattern matching version on line 14 uses `in` instead of `when`, and the pipe operator `|` indicates alternative patterns. While this example looks similar to traditional case statements, pattern matching's real power emerges when working with structured data.

```command
ruby app.rb
```

```text
[output]
medium
medium
```

The output shows both approaches producing identical results for simple value matching. Pattern matching really distinguishes itself when you need to look inside complex data structures, which we'll explore next.

Now that you understand the basic syntax, let's see how pattern matching handles array structures.

## Matching array patterns

Pattern matching excels at destructuring arrays and matching their contents:

```ruby
[label app.rb]
def describe_array(arr)
  case arr
  in []
    "empty array"
  in [x]
    "single element: #{x}"
  in [first, second]
    "two elements: #{first} and #{second}"
  in [first, *rest]
    "starts with #{first}, followed by #{rest.length} more"
  end
end

puts describe_array([])
puts describe_array([42])
puts describe_array([1, 2])
puts describe_array([1, 2, 3, 4])
```

The third line matches an empty array explicitly. Lines 5 and 7 demonstrate exact length matching - Ruby only enters these branches when the array has exactly one or two elements respectively. The pattern on line 9 introduces the splat operator `*rest`, which captures any remaining elements. This is particularly useful when you care about specific positions but want to handle variable-length arrays. The variable names (`x`, `first`, `second`, `rest`) automatically bind to the matched values, making them available in the branch body.

```command
ruby app.rb
```

```text
[output]
empty array
single element: 42
two elements: 1 and 2
starts with 1, followed by 3 more
```

The output confirms that pattern matching correctly identifies array shapes and extracts values. Notice how the last case captures the first element while grouping the remaining three elements into the `rest` array.

Beyond simple matching, you can also specify exact values that must appear in specific positions.

## Matching specific values in arrays

You can combine structural matching with value matching to create precise patterns:

```ruby
[label app.rb]
def analyze_coordinates(point)
  case point
  in [0, 0]
    "origin"
  in [0, y]
    "on y-axis at #{y}"
  in [x, 0]
    "on x-axis at #{x}"
  in [x, y]
    "point at (#{x}, #{y})"
  end
end

puts analyze_coordinates([0, 0])
puts analyze_coordinates([0, 5])
puts analyze_coordinates([3, 0])
puts analyze_coordinates([3, 4])
```

The third line shows how to match exact values - this pattern only matches when both elements are zero. Lines 5 and 7 demonstrate partial value matching: `[0, y]` matches arrays where the first element is exactly 0, while binding the second element to the variable `y`. The final pattern on line 9 acts as a catch-all for any two-element array, binding both values to variables. Ruby evaluates patterns in order, so more specific patterns should come before general ones.

```command
ruby app.rb
```

```text
[output]
origin
on y-axis at 5
on x-axis at 3
point at (3, 4)
```

The output shows how pattern matching routes each coordinate pair to the appropriate branch. The origin case matches before the more general patterns, demonstrating the importance of pattern ordering.

While arrays are straightforward, hash patterns introduce additional flexibility for working with key-value data.

## Matching hash patterns

Hash pattern matching is particularly powerful because it ignores extra keys by default:

```ruby
[label app.rb]
def process_user(user)
  case user
  in { name: "Admin" }
    "Administrator access"
  in { name: name, role: "moderator" }
    "Moderator: #{name}"
  in { name: name, age: age } if age >= 18
    "Adult user: #{name}"
  in { name: name }
    "User: #{name}"
  end
end

puts process_user({ name: "Admin", email: "admin@example.com" })
puts process_user({ name: "Alice", role: "moderator", level: 5 })
puts process_user({ name: "Bob", age: 25, verified: true })
puts process_user({ name: "Charlie", age: 16 })
```

The third line matches any hash containing `name: "Admin"`, regardless of what other keys exist. This is fundamentally different from exact hash comparison and makes pattern matching incredibly practical for real-world data. Line 5 shows simultaneous value matching and variable binding - we match the exact string "moderator" for role while capturing the name value. The guard clause on line 7 (`if age >= 18`) adds conditional logic beyond structural matching. Guards evaluate after the pattern matches, allowing you to add arbitrary conditions.

```command
ruby app.rb
```

```text
[output]
Administrator access
Moderator: Alice
Adult user: Bob
User: Charlie
```

The output demonstrates hash matching in action. Notice how Charlie's hash includes an age key but fails the guard condition, falling through to the general user pattern instead.

Sometimes you need to ensure a hash contains only specific keys, which requires a different approach.

## Exact hash matching

When you need to match hashes precisely without allowing extra keys, use the `**nil` pattern:

```ruby
[label app.rb]
def validate_config(config)
  case config
  in { host: host, port: port, **nil }
    "Valid config: #{host}:#{port}"
  in { host: host, port: port }
    "Config has unexpected keys"
  end
end

puts validate_config({ host: "localhost", port: 3000 })
puts validate_config({ host: "localhost", port: 3000, timeout: 30 })
```

The third line uses `**nil` to indicate that no additional keys should be present. This pattern only matches when the hash contains exactly `host` and `port` and nothing else. Without `**nil`, hash patterns are "open" by default, matching even when extra keys exist (as shown on line 5). This distinction is crucial when validating input or working with strict APIs that shouldn't accept unknown parameters.

```command
ruby app.rb
```

```text
[output]
Valid config: localhost:3000
Config has unexpected keys
```

The output shows the difference: the first hash matches the strict pattern, while the second hash fails because of the extra `timeout` key and falls through to the warning branch.

Pattern matching isn't limited to built-in types - it works beautifully with custom objects too.

## Matching object patterns

Pattern matching can destructure custom objects by calling their methods:

```ruby
[label app.rb]
class Point
  attr_reader :x, :y
  
  def initialize(x, y)
    @x = x
    @y = y
  end
  
  def deconstruct
    [x, y]
  end
  
  def deconstruct_keys(keys)
    { x: x, y: y }
  end
end

point = Point.new(3, 4)

case point
in Point(x: 0, y: 0)
  puts "Origin"
in Point(x: x, y: y)
  puts "Point at (#{x}, #{y})"
end
```

Lines 9-11 define the `deconstruct` method, which Ruby calls when matching array-like patterns. The method returns an array of values that represent the object's structure. Lines 13-15 define `deconstruct_keys`, which Ruby uses for hash-like patterns. By implementing these methods, you control exactly how your objects participate in pattern matching. The pattern on line 21 uses hash-style matching with the class name as a prefix, calling `deconstruct_keys` behind the scenes.

```command
ruby app.rb
```

```text
[output]
Point at (3, 4)
```

The output shows successful object pattern matching. Ruby automatically calls `deconstruct_keys` when it encounters the `Point(x: x, y: y)` pattern, extracting the values through the defined interface.

Beyond simple matches, you can use patterns to transform and validate nested data structures.

## Working with nested patterns

Pattern matching really shines when dealing with nested data structures:

```ruby
[label app.rb]
def process_response(response)
  case response
  in { status: 200, data: { user: { name: name, email: email } } }
    "Success: #{name} (#{email})"
  in { status: 200, data: data }
    "Success: #{data}"
  in { status: code, error: message } if code >= 400
    "Error #{code}: #{message}"
  in { status: code }
    "Response code: #{code}"
  end
end

response1 = { status: 200, data: { user: { name: "Alice", email: "alice@example.com" } } }
response2 = { status: 200, data: [1, 2, 3] }
response3 = { status: 404, error: "Not found" }

puts process_response(response1)
puts process_response(response2)
puts process_response(response3)
```

The third line demonstrates deep pattern matching - we're matching not just the outer hash structure, but diving three levels deep to extract `name` and `email` from the nested user object. This would require multiple conditional checks and nil guards with traditional approaches. The pattern on line 5 shows a fallback for successful responses that don't match the expected nested structure. Combining nested patterns with guards (line 7) creates sophisticated matching logic while keeping the code readable.

```command
ruby app.rb
```

```text
[output]
Success: Alice (alice@example.com)
Success: [1, 2, 3]
Error 404: Not found
```

The output demonstrates handling three different response shapes with a single case expression. The first response matches the deeply nested pattern, while the others fall through to their appropriate branches.

Sometimes you need to capture the entire matched value while also destructuring it, which the `as` pattern handles elegantly.

## Using the as pattern

The `=>` operator lets you capture a matched value while simultaneously destructuring it:

```ruby
[label app.rb]
def analyze_request(request)
  case request
  in { method: "GET", path: path => p }
    "GET request: #{p}"
  in { method: method, params: params } => full_request
    "#{method} with params: #{params}"
    puts "Full request: #{full_request.inspect}"
  end
end

puts analyze_request({ method: "GET", path: "/users" })
puts analyze_request({ method: "POST", params: { name: "Alice" }, headers: {} })
```

The third line uses `=> p` to capture the path value into the variable `p` after extracting it. This might seem redundant here since we're already binding it to `path`, but it becomes useful when matching complex nested structures where you want both the extracted value and the ability to reference it differently. Line 5 demonstrates capturing the entire matched hash with `=> full_request`, giving you access to both the destructured values and the original structure. This is particularly valuable when you need to log or pass along the complete data while working with specific fields.

```command
ruby app.rb
```

```text
[output]
GET request: /users
Full request: {method: "POST", params: {name: "Alice"}, headers: {}}
```

The output shows both the destructured values and the full request object. The `as` pattern provides flexibility when you need multiple views of the same data.

Pattern matching also supports the pin operator for matching against existing variables rather than creating new bindings.

## Pinning variables in patterns

The pin operator `^` tells Ruby to match against an existing variable's value instead of binding:

```ruby
[label app.rb]
def check_access(user, required_role)
  case user
  in { role: ^required_role, active: true }
    "Access granted"
  in { role: ^required_role }
    "Account inactive"
  in { role: role }
    "Access denied: has #{role}, needs #{required_role}"
  end
end

puts check_access({ role: "admin", active: true }, "admin")
puts check_access({ role: "admin", active: false }, "admin")
puts check_access({ role: "user", active: true }, "admin")
```

The third line uses `^required_role` to match against the value stored in the `required_role` parameter. Without the pin operator, Ruby would create a new variable named `required_role` and bind the hash value to it, which isn't what we want. The pin operator is essential when you need to compare against dynamic values rather than literal strings or numbers. Line 7 shows a regular binding without the pin operator, which captures whatever role value exists for display purposes.

```command
ruby app.rb
```

```text
[output]
Access granted
Account inactive
Access denied: has user, needs admin
```

The output demonstrates pinned variable matching. The first two cases match because the role matches the required value, while the third fails the pinned match and captures the actual role for the error message.

Beyond these core features, Ruby provides alternative ways to handle pattern matching failures.

## Handling non-matching cases

Pattern matching raises an exception if no pattern matches unless you provide an `else` clause:

```ruby
[label app.rb]
def safe_process(value)
  case value
  in Integer => n if n > 0
    "Positive: #{n}"
  in Integer => n if n < 0
    "Negative: #{n}"
  else
    "Zero or non-integer"
  end
end

def unsafe_process(value)
  case value
  in Integer => n if n > 0
    "Positive: #{n}"
  in Integer => n if n < 0
    "Negative: #{n}"
  end
end

puts safe_process(5)
puts safe_process("hello")

begin
  puts unsafe_process(0)
rescue NoMatchingPatternError => e
  puts "Error: #{e.message}"
end
```

The first function includes an `else` clause on line 7, which catches any values that don't match the previous patterns. This is the safe approach for pattern matching - always include an `else` or ensure your patterns are exhaustive. The second function (line 12) deliberately omits the `else` clause to show what happens when no pattern matches. Ruby raises a `NoMatchingPatternError`, which you can rescue if needed, but it's better to handle all cases explicitly.

```command
ruby app.rb
```

```text
[output]
Positive: 5
Zero or non-integer
Error: 0
```

The output shows the `else` clause catching unexpected values, while the unsafe version raises an error when encountering zero. Always providing an `else` clause prevents runtime errors and makes your code more robust.

For simple one-line pattern matching, Ruby offers a rightward assignment operator that's perfect for destructuring.

## Using rightward assignment

Ruby 3.0 introduced the rightward assignment operator `=>` for one-line pattern matching:

```ruby
[label app.rb]
# Destructure an array
[1, 2, 3] => [first, *rest]
puts "First: #{first}, Rest: #{rest.inspect}"

# Destructure a hash
{ name: "Alice", age: 30 } => { name: name, age: age }
puts "Name: #{name}, Age: #{age}"

# With objects
Point = Struct.new(:x, :y)
Point.new(5, 10) => Point(x, y)
puts "Coordinates: (#{x}, #{y})"
```

The second line shows rightward assignment destructuring an array. This is equivalent to a pattern match but doesn't require a case statement for simple destructuring operations. The syntax reads naturally: take this value and match it against this pattern, binding the variables. Line 6 demonstrates hash destructuring, which is particularly useful for extracting specific keys from larger hashes. The rightward assignment raises a `NoMatchingPatternError` if the pattern doesn't match, so use it when you're confident about the data structure.

```command
ruby app.rb
```

```text
[output]
First: 1, Rest: [2, 3]
Name: Alice, Age: 30
Coordinates: (5, 10)
```

The output confirms successful destructuring across different data types. Rightward assignment provides a concise syntax when you just need to extract values without conditional branching.

## Final thoughts

Pattern matching fundamentally changes how you handle complex data in Ruby. Instead of writing defensive code full of type checks and nil guards, you can declare the shapes you expect and let Ruby handle the matching. This declarative approach reduces bugs and makes your intentions immediately clear to anyone reading the code.

The key to effective pattern matching is starting simple and gradually incorporating more advanced features as needed. Begin with basic array and hash patterns, then introduce guards and nested matching as your confidence grows. To deepen your understanding, explore Ruby's official [pattern matching documentation](https://docs.ruby-lang.org/en/3.3/syntax/pattern_matching_rdoc.html) and experiment with refactoring existing conditional logic into pattern matching expressions.