I recently realised that despite many new additions to the Ruby language and ecosystem, I’ve never really had an opportunity to take advantage of many of them. Of course, some new language features are more useful than others, particularly when it comes to maintaining code as a team, but what is also interesting is that they also support less conventional, or immediately-apparent, use-cases.

The first part of this series of posts is all about /Pattern Matching/.

#+TOC: headlines 2

** Pattern matching

Ruby’s pattern matching support, introduced experimentally in 2.7, is a lot more powerful than you may expect. All you need is to replace ~when~ with ~in~ and your ~case~ statements become capable of matching against /anything/.

#+begin_src ruby require ‘base64’

class ParsedJson; end;

def handle_response(response) case response in { code: (300..399) } redirect_to response.headers[:location] in { status: :unauthorized | :forbidden => status } raise NotAllowedError.new(status) in { code: 200, body: ParsedJson => payload } Service.call(payload) in { code: 200, body: String => text, content_type: ‘application/base64’ } Base64.decode64(text) end end

handle_response({ code: 200, body: ‘aGVsbG8gd29ybGQ=’, content_type: ‘application/base64’ }) #=> ‘hello world’

handle_response({ code: 301 })

=> redirect

handle_response({ status: :forbidden, code: 403 })

NotAllowedError (forbidden)


** Custom destructuring

You can deeply match any object in Ruby so long as you define a method to represent it as a hash, or a method to represent it as an array. Or both.

#+begin_src ruby class PlayingCard attr_reader :value, :colour, :suit

def initialize(value:, colour:, suit:)
  @value = value
  @colour = colour
  @suit = suit

def deconstruct
  [value, colour, suit]

def deconstruct_keys(*)
    value: value,
    colour: colour,
    suit: suit

end #+end_src

This ~PlayingCard~ class is now capable of pattern matching.

#+begin_src ruby def face_card?(playing_card) case playing_card in { value: ‘K’ | ‘Q’ | ‘J’ } then true else false end end

face_card?(PlayingCard.new(value: ‘3’, colour: :red, suit: :spades)) #=> false #+end_src

** Pinning

That’s fairly basic, what about pattern matching poker? Matching one card is easy, but suppose you want to match a hand.

#+begin_src ruby class PokerHand attr_reader :cards

def initialize(cards: [])
  @cards = cards

def deconstruct

end #+end_src

Now that a hand of cards is represented, it should be possible to use pattern matching to find a winning play, say… a Royal Flush. For this to work, /variable pinning/ is required, because a Royal Flush requires the colour and suit to be the same for each card.

This particular solution depends on the hand being ordered, but that’s fine, a lot of computational problems become simpler if you sort them first. For the sake of example, assume that has already happened.

#+begin_src ruby def royal_flush?(hand) case hand in [[‘1’, c, s], [‘10’, ^c, ^s], [‘J’, ^c, ^s], [‘Q’, ^c, ^s], [‘K’, ^c, ^s]] true else false end end

alternatively, if golfing in Ruby 3:

def royal_flush?(hand) = !!(hand in [[‘1’, c, s], [‘10’, ^c, ^s], [‘J’, ^c, ^s], [‘Q’, ^c, ^s], [‘K’, ^c, ^s]] rescue false)

my_hand = PokerHand.new(cards: [ PlayingCard.new(value: ‘1’, colour: :black, suit: :hearts), PlayingCard.new(value: ‘10’, colour: :black, suit: :hearts), PlayingCard.new(value: ‘J’, colour: :black, suit: :hearts), PlayingCard.new(value: ‘Q’, colour: :black, suit: :hearts), PlayingCard.new(value: ‘K’, colour: :black, suit: :hearts), ])


=> true


The clever bit here is that the first part of the match (~[1, c, s]~) is used to constrain the rest of the pattern. So if ~c~ is ~:red~, then ~^c~ also has to be ~:red~ in order to match.

** Pattern guards

You’ll see this a lot if you’re familiar with Elixir or other languages that do pattern matching well. Essentially, you can add conditional logic to your patterns so that a match is only possible if a separate condition is met.

Building on the poker example, maybe it’s valid to play the Joker, but only if the dealer has allowed it?

#+begin_src ruby def joker_allowed? true end

def valid_call?(card) case card in [:Joker, *] if joker_allowed? puts ‘joker allowed’ true else true end end

valid_call?(PlayingCard.new(value: :Joker, colour: nil, suit: nil))

=> joker allowed

=> true


** Destructuring assignment without ~case~

One of the odd side-effects of this pattern matching functionality is that you get a new kind of assingment. In fact, in Ruby 3 this gets a syntax of its own with the rightward assignment operator, but you can still use something similar in 2.7.

In fact, this method also allows you to use pattern matching while destructuring. It’s not so easy on the eyes, however, as the variable bindings are actually inside the pattern, and not the expression on the left-hand side.

You also have to be absolutely sure you’re matching the right thing.

#+begin_src ruby card = PlayingCard.new(value: ‘7’, suit: :diamonds, colour: :red)

card in { value: (‘1’..‘10’) => v, suit: :diamonds => s}

v => ‘7’

s: :diamonds

begin card in { value: String, suit: Symbol } rescue NoMatchingPatternError puts ‘son, I am disappoint’ end #+end_src

** Optimisations

If you recall earlier examples, I defined ~destructure_keys(*)~, which meant that I was explicitly ignoring the arguments normally passed to the method. This is useful in simple cases, but when dealing with complex objects you might want to be a bit more thoughtful about how you return a value. For example, converting the entire structure of the object into a hash might not be appropriate.

#+begin_src ruby

When used in pattern matching, this class will only destructure into the provided keys

class PokerHand def deconstruct_keys(keys) cards.map { |card| card.slice(keys) } end end #+end_src

Well, this doesn’t cover the entirety of Ruby’s pattern matching fun, but it should at least show you the various things you’re now able to do with the feature. If in doubt, RTFM1; Ruby’s documentation is absolutely fantastic.

#+begin_aside Specifying ‘rubydoc’ in your Google searches should reveal Ruby’s official documentation and not the SEO spam that is ApiDock. #+end_aside

Check in soon to see another deep-dive into Ruby Sorcery.