Ruby 2.7 — Pattern Matching — Destructuring on Point
Brandon Weaver
Posted on June 1, 2019
Ruby 2.7 — Pattern Matching — Destructuring on Point
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:
Ruby 2.7 — Pattern Matching — First Impressions
This article will be a bit more structured.
Testing Warning!
Be sure to remember that variable assignment destructuring assigns local variables. This will mess with your testing unless you do it in methods instead of in a direct Pry
or IRB
session. We’ll dig into this more later in the article.
If something doesn’t match, it’s going to raise an error, so be sure to use else to handle default cases.
On Point!
One of the interesting things I’d noted was the ability to destructure from an object. Let’s say we have a Point
that has an x
and y
coordinate:
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
Destructuring is a means of extracting values from an object in Ruby. You may be familiar with the left-hand style from assignment:
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 about keywords?
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 Struct
s 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
Now I’d written a pattern matching library a while back, and I kinda want to see how it stacks up:
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
This was my first chance to play with some of pattern matching in Ruby, and I’m rather fond of it so far. There are some definite gotchas and a lot of syntax to take in, but there’s definitely a lot of power there.
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!
Posted on June 1, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.