This post represents a first attempt to evaluate the new pattern matching feature in Ruby 2.7 with a view to determining what use cases it might be a good fit for, both in general and in the context of Ruby.
Thanks to @mametter for corrections to this post.
Use case: processing JSON
This is perhaps the most directly useful application of pattern matching that I could find. Taken from this slide.
Given the following JSON data:
json = '{
"name": "Alice",
"age": 30,
"children": [
{
"name": "Bob",
"age": 2
}
]
}
'
We parse it, and want to extract the age of Bob, who is the child of Alice:
person = JSON.parse(json, symbolize_names: true)
case person
in {name: "Alice", children: [{name: "Bob", age: age}]}
p age #=> 2
end
An equivalent implementation without use of pattern matching, for comparison:
person = JSON.parse(json, symbolize_names: true)
if person[:name] == "Alice"
children = person[:children]
if children.length == 1 && children[0][:name] == "Bob"
p children[0][:age] #=> 2
end
end
The pattern matching implementation is more terse, and overall I find it easier to read. I like the simultaneous matching and extraction of values.
A more complex example–the following JSON has been heavily snipped but still demonstrates the ease with which data can be matched and extracted from multiple levels of a data structure:
json = <<~JSON
[
{
"id": 1,
"body": "I'm having a problem with this.",
"user": {
"login": "octocat"
},
"labels": [
{
"name": "bug"
}
],
"milestone": {
"description": "Tracking milestone for version 1.0",
"creator": {
"login": "octocat",
"site_admin": false
},
"open_issues": 4,
"closed_issues": 8,
"due_on": "2012-10-09T23:39:01Z"
},
"locked": true,
"active_lock_reason": "too heated",
"comments": 0,
"repository": {
"name": "Hello-World",
"owner": {
"login": "octocat"
},
"private": false,
"topics": [
"octocat",
"atom",
"electron",
"api"
],
"has_issues": true,
"has_projects": true,
"has_wiki": true,
"has_pages": false,
"has_downloads": true
}
}
]
JSON
We now parse the data and extract the topics, only if certain values are matched. First with pattern matching:
data = JSON.parse(json, symbolize_names: true)
case data
in [{id: 1, repository: {has_issues: true, owner: {login: 'octocat'}, topics: topics}}]
puts topics.inspect
end
And then a (possible) implementation without pattern matching:
if data.dig(0, :id) == 1 && data.dig(0, :repository, :has_issues) && data.dig(0, :repository, :owner, :login) == 'octocat'
puts data.dig(0, :repository, :topics).inspect
end
As with the simpler example above I find the pattern matching syntax to be much clearer. As the complexity of the data increases the advantage would similarly increase.
Example: Fibonacci
From this StackOverflow answer:
Haskell:
fib 0 = 1
fib 1 = 1
fib n | n >= 2
= fib (n-1) + fib (n-2)
Ruby with pattern matching:
def fib(n)
case n
in 0
1
in 1
1
in Integer => n if n >= 2
fib(n-1) + fib(n-2)
else
raise ArgumentError
end
end
(0..4).each { |n|
puts fib(n)
}
# puts fib(2.0) # ArgumentError
# puts fib(-1) # ArgumentError
Equivalent implementation without pattern matching:
def fib(n)
return 1 if [0, 1].include?(n)
raise ArgumentError unless Integer === n && n >= 2
fib(n-1) + fib(n-2)
end
Example: FizzBuzz
From this reddit post:
Any number divisible by three is replaced by the word Fizz and any divisible by five by the word Buzz. Numbers divisible by 15 become FizzBuzz.
(1..100).map do |n|
case [n.modulo(3), n.modulo(5), n]
in [0, 0, _]
"FizzBuzz"
in [0, _, _]
"Fizz"
in [_, 0, _]
"Buzz"
in [_, _, n]
n
end
end
Elixir-style pattern matching
From this blog post:
case File.open("hello.txt", [:write]) do
{:ok, file} ->
IO.write(file,"hello world")
File.close(file)
{:error, error} ->
IO.puts("Error openings the file: #{inspect error}")
end
By definining a wrapper around File.open we can implement this in Ruby as:
def open_file(path)
f = File.open(path, 'r')
[ :ok, f ]
rescue Errno::ENOENT => e
[ :error, e ]
end
case open_file('missing.txt')
in [ :ok, f ]
puts 'File found'
in [ :error, e ]
puts e.message
end
# Prints "No such file or directory @ rb_sysopen - missing.txt"
Conclusions
- As more standard types gain
deconstruct
support the feature will become more useful. - Pattern matching is essentially about deconstruction: if you do not need to deconstruct the value, or it does not support deconstruction, then pattern is perhaps not the right choice.
- Simultaneous matching and extraction of data from nested data structures (e.g. JSON) appears to me to be the strongest use case.
General Notes
- Match on
Array
is an exact match, match onHash
is a subset match, as this matches the general use cases. reference - The design of pattern matching has a long history stretching back to 2012. reference
- Just a few types have desconstruct/deconstruct_keys methods defined: (Array, Hash, Struct). reference