Pattern matching in Ruby

Agnieszka Małaszkiewicz at Fractal Soft

Ruby 2.7.0 preview 1

Experimental feature!

Pattern matching

a way to specify a pattern for our data and if data are matched to the pattern we can deconstruct them according to this pattern

Basic match operator in Elixir

```elixir iex> x = 4 4 iex> 4 = x 4 ```
```ruby irb> x = 4 => 4 irb> 4 = x Traceback (most recent call last): 3: from /home/agnieszka/.rvm/rubies/ruby-2.7.0-preview1/bin/irb:23:in `
' 2: from /home/agnieszka/.rvm/rubies/ruby-2.7.0-preview1/bin/irb:23:in `load' 1: from /home/agnieszka/.rvm/rubies/ruby-2.7.0-preview1/lib/ruby/gems/2.7.0/gems/irb-1.0.0/exe/irb:11:in `' SyntaxError ((irb):2: syntax error, unexpected '=', expecting end-of-input) 4 = x ^ ```

Pattern matching in Ruby - basics

```ruby case expression in pattern [if|unless condition] ... in pattern [if|unless condition] ... else ... end ```

Array

Pattern matching with Array

```ruby case [1, 2] in [2, a] :no_match in [1, a] :match end => :match irb> a => 2 ```

Splat operator

```ruby case [1, 2, 3, 4] in [1, *a] end => nil irb> a => [2, 3, 4] ```

Skip some values

```ruby case [1, 2, 3] in [_, a, 3] end => nil irb> a => 2 ```

Omit brackets

```ruby case [1, 2, 3] in 1, a, 3 end => nil irb> a => 2 ```

Complex structure of the Array

```ruby case [1, [2, 3, 4]] in [a, [b, *c]] end => nil irb> a => 1 irb> b => 2 irb> c => [3, 4] ```

Hash

Pattern matching in Hash

```ruby case { foo: 1, bar: 2 } in { foo: 1, baz: 3 } :no_match in { foo: 1, bar: b } :match end => :match irb> b => 2 ```

Double splat operator

```ruby case { foo: 1, bar: 2, baz: 3 } in { foo: 1, **rest } end => nil irb> rest => {:bar=>2, :baz=>3} ```

Omit brackets

```ruby case { foo: 1, bar: 2 } in foo: foo, bar: bar end => nil irb> foo => 1 irb> bar => 2 ```

Syntactic sugar

```ruby case { foo: 1, bar: 2 } in foo:, bar: end => nil irb> foo => 1 irb> bar => 2 ```

Exact match in array & Subset match in hash

Array

```ruby case [1, 2] in [1] :no_match end Traceback (most recent call last): 4: from /home/agnieszka/.rvm/rubies/ruby-2.7.0-preview1/bin/irb:23:in `
' 3: from /home/agnieszka/.rvm/rubies/ruby-2.7.0-preview1/bin/irb:23:in `load' 2: from /home/agnieszka/.rvm/rubies/ruby-2.7.0-preview1/lib/ruby/gems/2.7.0/gems/irb-1.0.0/exe/irb:11:in `' 1: from (irb):33 NoMatchingPatternError ([1, 2]) ```

Hash

```ruby case { foo: 1, bar: 2 } in foo: :match end => :match irb> foo => 1 ```

The same behavior in Hash like for Array

```ruby case { foo: 1, bar: 2 } in foo:, **rest if rest.empty? :no_match end Traceback (most recent call last): 4: from /home/agnieszka/.rvm/rubies/ruby-2.7.0-preview1/bin/irb:23:in `
' 3: from /home/agnieszka/.rvm/rubies/ruby-2.7.0-preview1/bin/irb:23:in `load' 2: from /home/agnieszka/.rvm/rubies/ruby-2.7.0-preview1/lib/ruby/gems/2.7.0/gems/irb-1.0.0/exe/irb:11:in `' 1: from (irb):37 NoMatchingPatternError ({:foo=>1, :bar=>2}) ```

Guards

Guard condition

```ruby case [1, 2, 3] in [a, *c] if a != 1 :no_match in [a, *c] if a == 1 :match end => :match irb> a => 1 irb> c => [2, 3] ```

What can we use in pattern matching?

Literals

```ruby case 2 in (1..3) :match in Integer :too_late_for_match end => :match ```

Variables

```ruby irb> array = [1, 2, 3] => [1, 2, 3] case [1, 2, 4] in array :match end irb> array => [1, 2, 4] ```

^ operator

```ruby irb> array => [1, 2, 4] case [1, 2, 3] in ^array :no_match else :match end irb> array => [1, 2, 4] ```

Alternative pattern

```ruby case 5 in 6 :no_match in 2 | 3 | 5 :match end => :match ```

As pattern

```ruby case [1, 2, [3, 4]] in [1, 2, [3, b] => a] end => nil irb> a => [3, 4] irb> b => 4 ```

Pattern matching for others objects

Struct

```ruby Point = Struct.new(:latitude, :longitude) point = Point[50.29543618146685, 18.666200637817383] case point in latitude, longitude end => nil irb> latitude => 50.29543618146685 irb> longitude => 18.666200637817383 ```

Pattern matching for custom objects

`deconstruct` or `deconstruct_keys`

Date

```ruby class Date def deconstruct_keys(keys) { year: year, month: month, day: day } end end date = Date.new(2019, 9, 21) case date in year: end => nil irb> year => 2019 ```

JSON

Data example

```ruby { "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": {}, "geometry": { "type": "Point", "coordinates": [ 18.666200637817383, 50.29543618146685 ] } } ] } ```

JSON with if statements

```ruby point = JSON.parse(json, symbolize_names: true) if point[:type] == "FeatureCollection" features = point[:features] if features.size == 1 && features[0][:type] == "Feature" geometry = features[0][:geometry] if geometry[:type] == "Point" && geometry["coordinates"].size == 2 longitude, latitude = geometry["coordinates"] end end end irb> longitude => 18.666200637817383 irb> latitude => 50.29543618146685 ```

JSON with pattern matching

```ruby case JSON.parse(json, symbolize_names: true) in { type: "FeatureCollection", features: [{ type: "Feature", geometry: { type: "Point", coordinates: [longitude, latitude] }}]} end irb> longitude => 18.666200637817383 irb> latitude => 50.29543618146685 ```

Scope strange behavior

Problem with scope

```ruby case[1, 2] in x, y if y > 3 :no_match in x, z if z < 3 :match end => :match irb> x => 1 irb> z => 2 # unexpected assignment for y when pattern matching failed irb> y => 2 ```

What I would like to see?

It will be nice to have

- one line pattern matching ```ruby case [1, 2, [3, 4]] { [1, 2, [3, b] => a] } ``` - calculations in patterns ```ruby in (1..3).to_a ``` - allowed variables in alternative pattern ```ruby [1, 2] | [1, 2, c] ```

Summary

Links

Woman on Rails logotype

Agnieszka Małaszkiewicz

agnieszka (at) fractalsoft (dot) org