Ruby 2.7 — Pattern Matching — First Impressions

Shall we grab a sneak peak at what Pattern Matching looks like in Ruby 2.7? The code’s merged, nightly build is on the way, and I’m a rather impatient writer who wants to check out the new presents.

This is my first pass over pattern matching, and there will be more detailed articles coming out as I have more time to experiment with the features.

To be clear: This article will meander as it’s a first impression. I’ll be running more tests on it tomorrow, but wanted to write something on it tonight to see how well first impressions matched up.

The Short Version

This is going to be a long article, and the short version isn’t going to cover even a small portion of it. Each major section will have a single style it covers.

The test file covers a good portion of this if you want an at-a-glance read

Literal Matches

Much like a regular case statement, you can perform literal matches:

case 0
in 0 then true
else false
end
# => true

In these cases it would probably be best to use when instead.

The difference is that if there’s no match it’s going to raise a <a href="https://github.com/ruby/ruby/blob/9738f96fcfe50b2a605e350bdd40bd7a85665f54/test/ruby/test_pattern_matching.rb#L83" target="_blank">NoMatchingPattern</a> error rather than returning nil like case / when statements would.

Multiple Matchers

In typical case statements, a comma would be used, but with some of the new syntax this would break some of the destructuring that will be mentioned later:

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

In the case of captured variables below, they can’t be mixed:

case 0
in a | 0
end
# Error: illegal variable in alternative pattern

Captured Variables

One of the more interesting additions is captured variables:

case true
in a
  a
end
# => true

In Guard Clauses

These can also be used in suffix conditionals or rather guard type clauses in what’s reminiscent of Haskell:

case 0
in a if a == 0
  true
end
# => true

In Assignment Mappings

They can even be assigned with a Hash-like syntax:

case 0
in 0 => a
  a == 0
end

With Placeholders

Most interesting in this section is that they appear to treat underscores as a special placeholder:

case 0
in _ | _a
  true
end

Though this could also be seen as a literal variable assignment, it’s an interesting riff on Scala’s pattern matching wildcards.

Destructuring

Interestingly you can shadow the variables as well while destructuring an array:

case [0, 1]
in a, a
  a == 1
end
# => true

It appears that the last assignment will be a in this case, and that commas are now used for destructuring rather than for denoting a separate pattern to match against as is the case for when.

Deconstruct

In the top of the file there’s a class defined:

class C
  class << self
    attr_accessor :keys
  end
  def initialize(obj)
    @obj = obj
  end
  def deconstruct
    @obj
  end
  def deconstruct_keys(keys)
    C.keys = keys
    @obj
  end
end

It appears to indicate a way to define how an object should be destructured by pattern matching:

[[0, 1], C.new([0, 1])].all? do |i|
  case i
  in 0, 1
    true
  end
end

including accounting for unbalanced matches:

[[0], C.new([0])].all? do |i|
  case i
  in 0, 1
  else
    true
  end
end

I don’t understand how deconstruct_keys is working, but it feels really odd to me from first glance.

With Splats

It also will capture multiple arguments like destructuring currently works on Ruby arrays:

case []
in *a
  a == []
end
case [0, 1, 2]
in *a, 1, 2
  a == [0]
end

Meaning that * could also be used as a bit of a wildcard:

case 0
in *
  true
end

On Hashes

These appear to work a lot like keyword arguments:

case {a: 0}
in a: 0
  true
end

That also means that kwsplats could be used here:

case {}
in **a
  a == {}
end

…and with the keywords bit above, that means that classes could define their own destructuring using keywords, but I’m not clear on that.

I’m really hoping that this also uses === on each param, but the tests don’t indicate this. I’ll be experimenting with this later.

Triple Equals

Like our current case statements, it appears that the new method of pattern matching uses <a href="https://github.com/ruby/ruby/blob/9738f96fcfe50b2a605e350bdd40bd7a85665f54/test/ruby/test_pattern_matching.rb#L255" target="_blank">===</a> for comparisons:

case 'abc'
in /a/
  true
end
case 0
in -> i { i == 0 }
  true
end

Combined with Destructuring

One thing I enjoyed in Qo was the ability to match against another array using === to write queries. It looks like the new syntax will compare each element and give us something very similar:

case [0, 1, 2, 3, 4, 5]
in [0..1, 0...2, 0.., 0..., (...5), (..5)]
  true
end

It’s amusing that they’ve included beginningless and endless ranges in the example, as that could be very useful later on. I certainly hope these work with hashes, as I really really really want this to work:

case object
in method_a: 0..10, method_b: String
  true
end

…because if we can define our own deconstructors just imagine the possibilities there. The class attr for keys is odd though, not sure what to think of that one.

Pin Operator

I’m going to have to come back to this one as I do not understand it. I assume it means not to assign and to “pin” a variable so it doesn’t get overwritten:

a = /a/
case 'abc'
in ^a
  true
end
case [0, 0]
in a, ^a
  a == 0
end

I believe this can be used to capture one element in a variable and also reference it later like a back-reference of sorts.

Thoughts?

This hasn’t built with Nightly yet, so I intend to make a second pass on this after it clears to verify some behavior I have suspicions about. Mostly I want to see about the hash matchers, as there’s some amazing potential there.

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.

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 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

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

Thanks for reading ❤

If you liked this post, share it with all of your programming buddies!

#ruby #function

Ruby 2.7 — Pattern Matching 
1 Likes70.25 GEEK