Ruby 2.7 — Pattern Matching — Destructuring on Point

Brandon Weaver
Apr 18 · 5 min read

Now that pattern matching has hit Ruby Nightly as an experimental feature, let’s take a look into some potential usecases for it starting with Destructuring.

If you haven’t seen the first article going over a lot of the spec, you can find it here:

This article will be a bit more structured.

Testing Warning!

If something doesn’t match, it’s going to raise an error, so be sure to use else to handle default cases.

On Point!

Point = Struct.new(:x, :y) do
def deconstruct
self.to_a
end

def deconstruct_keys(keys)
self.to_h
end
end

We’ll use this as our base example for now.

Array Destructuring

x, y = Point.new(0, 1).to_a
x # => 0
y # => 1
*coords = Point.new(2, 3).to_a
coords # => [2, 3]

These are all valid in in expressions in a pattern matching context. That includes splatting values.

Direct Value

We can destructure to match directly against values:

case Point.new(0, 1)
in 0, 1 then Point.new(0, 2)
end
=> #<struct Point x=0, y=2>

These items all respond to === as we’ll see later in this article.

Triple Equals Destructuring

Anything that responds to === is perfectly fair game here:

case Point.new(0, 1)
in 0.., 1.. then Point.new(0, 2)
end

Direct Variable

If we wanted to just move north, we can use pattern matching to pull out our x and y values by position:

case Point.new(0, 1)
in x, y then Point.new(x, y + 1)
end
=> #<struct Point x=0, y=2>

So it looks like the then keyword is still valid here. Good to know.

Now something interesting is also happening here. It’s assigning local variables, meaning after that statement this works:

[x, y]
=> [0, 1]

This works with any of the variable assignment styles, and caught me a bit by surprise though it does make sense.

Triple Equals Destructuring into Variables

How about if we have some ranges?

case Point.new(0, 1)
in 0..5 => x, 0..5 => y
Point.new(x, y + 1)
end
#<struct Point x=0, y=2>

What’s important to note here is that the format is:

value or matcher => destructured variable

These will respond to anything implementing === , which is what makes case statements so powerful in Ruby. Read this for more information on ===:

Keyword Destructuring

What we don’t necessarily get in Ruby is the ability to natively destructure on keywords, but with pattern matching we can if and only if deconstruct_keys is defined and returns a hash-like object like above:

def deconstruct_keys(keys)
self.to_h
end

I’m not sure what keys are doing here, I’ll have to take a TracePoint at this to try and find out what’s going on later. If you have ideas let me know!

Considering Structs kind-of already do some of this, that’s an interesting technicality but not one we’ll worry about for now.

Keywords are not Variable Assignments

The thing to be careful of here is that the keys are used for destructuring, but not assignment:

case Point.new(0, 1)
in x: 0, y: 1..5 then Point.new(x, y + 1)
end
NameError: undefined local variable or method `x' for main:Object

Arrows are still used for Assignment

So that doesn’t work. We have to use => here to bind them to a local variable:

case Point.new(0, 1)
in x: 0 => x, y: 1..5 => y then Point.new(x, y + 1)
end
=> #<struct Point x=0, y=2>

This means we get full access to === here as well, which can be very useful.

Emulating Qo — Preview

This is a preview of some of the next article.

Let’s start with a Person:

Person = Struct.new(:name, :age) do
def deconstruct
self.to_a
end

def deconstruct_keys(keys)
self.to_h
end
end

We’ll also be using the Any gem for a wildcard:

Name is Longer than 3 Characters

The Qo way:

name_longer_than_three = -> person { person.name.size > 3 }

people_with_truncated_names = people.map(&Qo.match { |m|
m.when(name_longer_than_three) { |person|
Person.new(person.name[0..2], person.age)
}
m.else
})

The Pattern Matching way:

person = Person.new('Edward', 20)case person
in name: -> n { n.size > 3 } => name, age: Any => age
Person.new(name[0..1], age)
else
person
end
=> #<struct Person name="Ed", age=20>

Wrap Up

There are also some very odd things like keys I don’t understand, and what happens if you try and do anything fancy in a pattern match like this:

case Point.new(0, 1)
in x: :even?.to_proc => x then Point.new(0, 0)
end
endSyntaxError: unexpected '.', expecting `then' or ';' or '\n'
in x: :even?.to_proc => x then Point.new(0...

I believe this is likely a bug in the parser, but as this is experimental that’s to be expected.

My next few dives into pattern matching will likely follow trying to emulate various features I’d used Qo for:

2.7 is shaping up to be a very interesting release. Let’s see where it goes from here!

Brandon Weaver

Written by

Ruby and Javascript at Square. Typically responds in puns. Opinions are my own.