Menu

Call Us0333 0146 683
Tech blog

Pattern matching in Ruby 2.7

2-minute read

Lewis Jones

Lewis Jones

20 January 2020

One of the most interesting new features of the latest release of Ruby is pattern matching. Here's a quick tour of some of its features.

The concept of pattern matching comes from functional languages. The implementer of pattern matching in Ruby - Kazuki Tsujimoto, used this quote about pattern matching, as experienced in Haskell, to explain it.

"Pattern matching consists of specifying patterns to which some data should conform and then checking to see if it does and deconstructing the data according to those patterns." -

Learn You a Haskell for Great Good! (Miran Lipovaca)

Ruby's version of pattern matching looks very similar to Elixir's. One difference however is that Ruby pattern matching can only be used inside case statements. It's too complex for the Ruby core team to implement method / function overloading as functional languages use.

The following examples use Ruby version 2.7.0. Be warned: pattern matching is an experimental feature that may change in future versions. You'll see a warning every time your interpreter runs some Ruby with pattern matching.

Let's look at it in action

Pattern matching allows you to match the data structure of the argument you pass to a case statement and use that to assign variables.

array = [1,2,3]
case array
  in [a, _, _]
  p a #=> 1
end

We've pattern matched the first element of the array and assigned it to the variable a. The underscore _ is used as the placeholder.

If we don't care about or don't need the length of the array but want specific elements, we can use the splat operator (*).

array = (1..100).to_a
case array
  in [ a, _, c, *]
  p a #=> 1
  p c #=> 3
end

You can also pattern match on splats to obtain the tail of an array.

array = [1,2,3]
case array
  in [ a, *tail]
  p tail #=> [2,3]
end

A powerful tool for nested hashes such as JSON

We have a number of event-driven systems at Simply Business where we consume domain events in JSON from our event bus.

Given the JSON below, we want the quote_id from the payload if the payload type is "online" and there is a quote and the quote is a renewal.

{
  "event_name": "quote_updated",
  "payload": {
    "id": "123",
    "type": "online",
    "quote": {
      "renewal": true,
      "id": "456"
    }
  }
}

With pattern matching, that's expressive and simple.

case JSON.parse(json, symbolize_names: true)
  in { payload: { type: "online", quote: { renewal: true, id: quote_id } } }
  p quote_id #=> 456
end

What I like about this is that you don't need to understand the underlying implementation of pattern matching to see what it is doing.

Further syntax

The pattern is run in sequence until a match is found. If no pattern is matched, the else clause is executed. If no pattern is matched and there is no else clause then the NoMatchingPatternError is raised.

case expr
in pattern [if | unless condition]
  ...
in pattern [if | unless condition]
  ...
else
  ...
end

Notice that you can use conditions along with patterns.

Alternative pattern

The | operator can be used to add multiple possible matches to the same in.

case 0
in 0 | 1 | 2
  true #=> true
end

Simply Business technology

Pin operator

Inspired by Elixir, the ^ operator can be used to pin the value of a variable of the same name that has been defined previously.

This returns true, as the second a with the pin is looking for the same value - the first a, was defined as:

case ['foo', 'foo']
  in [a, ^a]
    true #=> true
end

This errors as it was expecting the second element to have the same value as the first value captured by the pinned a.

case ['foo', 'bar']
  in [a, ^a]
    true #=> NoMatchingPatternError (["foo", "bar"])
end

Use hash pattern matching on your own objects

To use hash pattern matching on your own objects, they need to implement #deconstruct_keys. This needs to return a hash containing the values you wish to use in your patterns.

In the example below, this allows us to pattern match on the attributes of the shape and also on a custom matching key area, as it's returned in the #deconstruct_keys hash.

class Shape
  ATTRIBUTES = [:sides, :width, :height]

  attr_accessor :sides, :width, :height

  def deconstruct_keys(keys)
    (ATTRIBUTES & keys).each_with_object({}) do |k, v|
      h[k] = send(v)
    end.merge({ area: width * height})
  end
end

square = Shape.new
square.sides = 4
square.width = 4
square.height = 4

case square
 in { sides: 4 }
   p true #=> true
end

case square
 in { area: 16 }
   p true #=> true
end

Summary

Pattern matching is an interesting feature and I definitely see how it could be used to make it simpler to parse and extract values from complex data structures such as nested hashes.

Remember - it's still an experimental feature that will warn you each time you use it.

If you want to learn more, I suggest you read the official Ruby 2.7 documentation.

Ready to start your career at Simply Business?

Want to know more about what it's like to work in tech at Simply Business? Read about our approach to tech, then check out our current vacancies.

Find out more

Find this article useful? Spread the word.

Share
Tweet
Post

Keep up to date with Simply Business. Subscribe to our monthly newsletter and follow us on social media.

Subscribe to our newsletter

© Copyright 2020 Simply Business. All Rights Reserved. Simply Business is a trading name of Xbridge Limited which is authorised and regulated by the Financial Conduct Authority (Financial Services Registration No: 313348). Xbridge Limited (No: 3967717) has its registered office at 6th Floor, 99 Gresham Street, London, EC2V 7NG.