Taking pattern matching further in Ruby 2.7
Tags: ruby, functional
Published: 2019-12-22 23:11:23

With Ruby 2.7 just around the corner, and release candidate 2 [available now](https://www.ruby-lang.org/en/news/2019/12/21/2-7-0-rc2-released/), I was eager to try the new pattern matching feature that has been introduced. As with anything in Ruby, I wanted to see how I could push this feature to its limits.

Note that this is not an introduction to Ruby 2.7's pattern matching; make sure you're already familiar with its purpose and how to use it. The [release notes](https://www.ruby-lang.org/en/news/2019/12/21/2-7-0-rc2-released/) summarise this very well!

# How does pattern matching work?

There are two main methods involved with the behaviour behind pattern matching, `#deconstruct` and `#deconstruct_keys`, which are used for array-style matches and hash-style matches respectively.

`#deconstruct` returns an array representing the object which can be matched against. For the `Array` class, this is simply aliased to `#itself`, as no conversion needs to be done here. Instances of `Struct` define `#deconstruct` as an array of the field values.

```ruby
[1, 2, 3].deconstruct # => [1, 2, 3]

Person = Struct.new(:name, :favourite_food)
Person.new('Aaron', 'Pizza').deconstruct # => ['Aaron', 'Pizza']
```

`#deconstruct_keys` is similar, except it returns a hash rather than an array. Additionally, `#deconstruct_keys` takes an array of Symbol keys as an argument, indicating which keys of the hash must be returned. It is permitted to a hash with more keys than specified (`Hash#deconstruct_keys` is aliased to `#itself` so returns all keys) but the additional keys won't be used for the pattern match.


```ruby
{ a: 3, b: 4 }.deconstruct_keys([:a, :b]) # => { a: 3, b: 4 }

Person = Struct.new(:name, :favourite_food)
me = Person.new('Aaron', 'Pizza')
me.deconstruct_keys([:name]) # => { name: 'Aaron' }
me.deconstruct_keys([:name, :favourite_food]) # => { name: 'Aaron', favourite_food: 'Pizza' }
```

# As the `case` may be...

I was surprised to learn that you don't actually need to use the `in` operator for pattern matching within a `case` statement; you can use it anywhere! This can be used to unpack structs quite elegantly:

```ruby
Person = Struct.new(:name, :favourite_food)
me = Person.new('Aaron', 'Pizza')

me in { name: my_name, favourite_food: my_favourite_food }
my_name # => 'Aaron'
my_favourite_food # => 'Pizza'
```

You can even use it as a slightly esoteric form of assertion, as a pattern which doesn't match will raise a `NoMatchingPatternError`. I'm not sure I'd recommend this, but it certainly works!

```ruby
x = 3
x in 3 # Fine!
x in 2 # Raises NoMatchingPatternError
```

In its most basic form, you can even use pattern matching as a simple assignment. I wonder if DSLs could make use of this?

```ruby
2 in y
y # => 2
```

# A methodical approach

This was inspired by [Brandon Weaver's excellent post](https://medium.com/@baweaver/ruby-2-7-pattern-matching-first-impressions-cdb93c6246e6) about pattern matching. Being able to match against objects which define `#deconstruct` or `#deconstruct_keys` is great, but what about any other objects where properties are behind methods?

It's really easy to create a wrapper around any object which will dynamically invoke methods on it when `#deconstruct_keys` is called, allowing any object's zero-argument methods to be used in pattern matching!

Let's create an example of how this will help us first. I'll define a simple data class:

```ruby
class Rectangle
  def initialize(width, height)
    @width = width
    @height = height
  end

  attr_accessor :width, :height
end
```

`Rectangle#width` and `Rectangle#height` are methods which will retrieve the properties of any instances of `Rectangle`. Unfortunately, pattern matching doesn't know how to match against instances of this class, as it doesn't contain a `#deconstruct_keys` definition:

```ruby
rect = Rectangle.new(2, 5)
rect in { width: w, height: h } # Raises NoMatchingPatternError
```

Let's define a class which can wrap any object in to allow method calls to be made from a pattern match. I'll call it `SendMatch`:

```ruby
class SendMatch
  def initialize(target)
    @target = target
  end

  def deconstruct_keys(keys)
    keys.to_h { |key| [key, @target.send(key)] }
  end
end
```

How this works is actually really simple. When constructed, it takes a single object and stores a reference to it as an instance variable `@target`. Then, when `#deconstruct_keys` is called, it invokes each the methods with the names of each requested key on `@target` and creates a hash from the results.

This effectively lets pattern matching work on any object!

```ruby
rect = Rectangle.new(2, 5)
SendMatch.new(rect) in { width: w, height: h }
w # => 2
h # => 5
```

We could make this even more concise by monkey-patching `Object`, so classes without a definition of `#deconstruct_keys` will automatically call methods instead:

```ruby
class Object
  def deconstruct_keys(*args)
    SendMatch.new(self).deconstruct_keys(*args)
  end
end

rect in { width: w, height: h }
w # => 2
h # => 5
```

-------

I hope this has given you a good insight into how pattern matching can be used in creative ways in Ruby 2.7! Remember that pattern matching is still an experimental feature, so any of these behaviours could change at any time.

I'd love to hear about how you're using pattern matching to streamline your Ruby code!