Find your way in Ruby Pattern matching
Tags: ruby, pattern, matching, beginners
Published: 2020-03-01 01:58:25


Last December we got for Christmas [Ruby 2.7.](https://www.ruby-lang.org/en/news/2019/12/25/ruby-2-7-0-released/) release and between its many interesting features and improvements, one in specific is pretty much a first for the ruby language.

This article is an overview of it, ideal for ruby beginners curious about Ruby new features. 


##Pattern Matching

The experimental feature Pattern matching is, in the words of its creator, 'a combination of case/when and multiple assignments' and, if you are coming from Javascript, it might look like a quite lean switch statement.

That means we now can combine deconstruct arrays and hashes, and with matching its items with specific patterns or classes.

Pattern matching can be especially powerful when working with complex objects, like parsed JSON, YAML files, or even [HTTP routers](https://medium.com/@retrorubies/ruby-2-7-experimental-pattern-matching-in-few-examples-4d0987b9e12f).

That all sounds very cool and all, but first let's play a bit with the basics of the feature.

----

##Value pattern

Let's get used to the basic syntax. 
It starts with the keyword `case` followed by the object or variable you will be matching.

```case 67```

Then we add the first matching case with the keyword `in` in the left.
In this one, we want to test if 67 is between 0 and 100.

```ruby
case 67
  in 0..100
```

This is the only possibility we want to match for right now, so let's ask it to print something if it matches and close it with the `end` keyword.

```ruby
case 67
  in 0..100
    p 'it\'s a match!'
end
```
It should output `"It's a match"` since the condition we matched for was true.

What if it's not the case though?
Let's test it again but with a number in the case that will fail the matching.

```ruby
case 167
  in 0..100
    p 'it\'s a match!'
end
=> NoMatchingPatternError (167)
```

When the matching fails it outputs a NoMatchingPatternError followed by the tested object inside a parenthesis.

Let's handle when we don't have a match with an `else`.

```ruby
case 167
  in 0..100
    p 'it\'s a match!'
  else
   p 'Number not in range :-('
end
=> "Number not in range :-("
```

That's way better than getting an error.

It's possible to add multiple matching conditions too.

```ruby
case 80
  in 0..10
    p 'A small number'
  in 11..50
    p 'An okay sized number'
  in 51..100
    p 'A respectable sized number'
  else
   p 'Number not in range :-('
end
=> "A respectable sized number"
```

-----

##Variable pattern
 
What if we want to print the value that got matched itself?

That's when binding a variable to it comes in handy. Let's start simple.

```ruby
case 100
  in num
    p "num got its name bound to #{num}"
end
=> "num got its name bound to 100"
```

And now we can combine the variable binding with the matching.

```ruby
case 100
  in Integer => num 
    p "Bind #{num} only if matched"
end
=> "Bind 100 only if matched"
```

In the example above we first make sure that 100 is an Integer, and if that's true we bind its value to the variable num and print the message.


------

##Alternative pattern

Sometimes more than one condition might be good.

```ruby
make_it = 'better'

case make_it
  in 'harder' | 'better' | 'faster' | 'stronger' => like_this
    p "Do it #{like_this}"
end
=> "do it better"
```

##As pattern

It's also possible to test regular expressions

```ruby
bad_email = 'wrongmailATmail.com'

case bad_email
  in /.+@.+\.\w/ => email
    p "Sending to #{email}"
  else
    p 'This is not an email'
end
=> "This is not an email"
```

```ruby
good_email = 'rightmail@mail.com'

case good_email
  in /.+@.+\.\w/ => email
    p "Sending to #{email}"
end
=> "Sending to rightmail@mail.com"
```

----


##With Arrays

The real fun with Pattern matching starts when you use it with objects though. It's possible to match an array all the ways in the example below.

```ruby
case [0, 1, 2]
  in Array(0, 1, 2) => arr
    p arr
end

case [0, 1, 2]
  in Object[0, 1, 2] => arr
    p arr
end

case [0, 1, 2]
  in [0, 1, 2] => arr
    p arr
end

=> [0, 1, 2]
```

Be careful with combining syntax sugar and variable binding though.

```ruby
case [0, 1, 2]
  in 0, 1, 2 => arr
    p arr
end
=> 2

```

By default, arrays match exactly. The code bellow should fail the matching.

```ruby
arr = [0, 1, 2]

case arr
  in [1, 2] => a
    p a
end
=> NoMatchingPatternError ([0, 1, 2])
```

If we don't care for the first item the array we can use `_`. Now it should only test the second and last array element and ignore the value of the first one.

```ruby
arr = [0, 1, 2]

case arr
  in [_, 1, 2] => a
    p a
end
=> [0, 1, 2]
```

Or even better, we can match only a subset of the array by using `*`. Now it will only try to match the last array element with the number 2.

```ruby
arr = [0, 1, 2]

case arr
  in [*, 2] => a
    p a
end
=> [0, 1, 2]
```

---

We can also bind variables to array elements.


```ruby
user = [7271, ['Marcela', 'Pena', 26]]

case user
  in [id, [first_name, last_name, age]]
    p "User #{id}, #{first_name} #{last_name}, is #{age} years old"
end
=> "User 7271, Marcela Pena, is 26 years old"
    
```

Let's ignore some of the values with `_`, and use a guard clause to check if the user is of age.

```ruby
user = [7271, ['Marcela', 'Pena', 26]]

case user
  in [_, [first_name, _, age]] if age >= 18
    p "#{first_name} is #{age} years old"
  else
    p "User is underage"
  end
end
=> "Marcela is 26 years old"
    
```


----

##With hashes

```ruby
my_hash = {a: 0, b: 1, c: 2}

case my_hash
  in Hash(a: a, b: 1) => h
   p h
end
=> {:a=>0, :b=>1, :c=>2}

case my_hash
  in Object[a: a, b: 1] => h
   p h
end
=> {:a=>0, :b=>1, :c=>2}

case my_hash
  in {a: a, b: 1} => h
   p h
end
=> {:a=>0, :b=>1, :c=>2}

# careful if you go no brackets though
case my_hash
  in a: a, b: b => h
   p h
end
=> 1

```

Hash patterns are more flexible than array matches, by default they always test for subsets.

```ruby
my_hash = {a: 0, b: 1, c: 2}

case my_hash
  in {b: 1}
    p rest
end
=> [[0, 1, 2]]

case {a: 0, b: 1, c: 2}
  in a:, b:
    p a
    p b
end
=> 0
=> 1
```

It's possible to make exact matches with hashes though.

```ruby
my_hash = {a: 0, b: 1, c: 2}

case my_hash
  in {a: 0, **rest} if rest.empty?
    p a
end
=>NoMatchingPatternError ({:a=>0, :b=>1, :c=>2})


other_hash = {a: 0}

case other_hash
  in {a: 0, **rest} if rest.empty?
    p a
end
=> 0
```

And power it up with deconstruction

```ruby
user_hash = {id: 189, name: {first_name: 'Alanis', last_name: 'Morris'}, nick_name: nil, pets: {cats: ['Flufbell', 'Lady Merlin'], dogs: []}}

case user_hash
  in {id: nil => id, name: }
    # save user if it doesn't have an id and has a name
    p 'Save new user!'
  in {name:, nick_name: String => nick_name}
    p "#{nickname} is all set up"
  in {name: {first_name:, last_name:}, nick_name: String => nick_name, pets: {cats:, dogs:}} if cats.empty? && !dogs.empty?
    p "#{first_name} nick name will be #{last_name}-woof"
  in {name: {first_name:}, nick_name: nil => nick_name, pets: {cats:, dogs:}} if !cats.empty? && dogs.empty?
    p "#{first_name} nick name will be Catty#{first_name}"
  in {name: {first_name:}, nick_name: nil => nick_name, pets: {cats:, dogs:}} if !cats.empty? && !dogs.empty?
  p "#{first_name} nick name will be #{first_name}Meoof"
  else
    'Sign up'
  end
=> "Alanis nick name will be CattyAlanis"

```

Now that you got used to the basics of pattern matching and really want to go into the meat of it make sure to read the [official documentation](https://docs.ruby-lang.org/en/master/syntax/pattern_matching_rdoc.html) and [other awesome](https://dev.to/akshaynathan/ruby-2-7-experimental-features-in-production-pattern-matching-and-numbered-block-args-jcg) [in-depth](https://medium.com/cedarcode/ruby-pattern-matching-1e84cab3b44a) [articles](https://www.toptal.com/ruby/ruby-pattern-matching-tutorial) written [about it](https://medium.com/@baweaver/ruby-2-7-pattern-matching-first-impressions-cdb93c6246e6) and to watch Kazuki Tsujimoto presenting it at [RubyConf 2019](https://www.youtube.com/watch?v=wo4eZ2iVIyo).

Let me know if this article was useful for you or if it can be improved. 
Happy codding!