As it’s a tradition, we got a new Ruby version on Christmas. This time we are getting pattern matching, a feature highly praised in other languages. After spending some time with Elixir last year I was curious how does Ruby’s pattern matching feel in the Ruby world and indeed how does it compare to Elixir’s?

Ruby 2.7 is adding a new case/in syntax to support pattern matching. This also tells us, that this (so far experimental) feature is only available for our case statements. Let’s look on some Ruby code and how Elixir case statement makes it cleaner with pattern matching. In the first example we want to find a name of a parent called Alice and only as long as the kid is a single child. In the second we want to find the name of the parent of single child called Bob.

# Ruby require 'json' json = << END { "name": "Alice", "age": 30, "children": [{ "name": "Bob", "age": 2 }] } END # Finding the child json = JSON . parse ( json ) if json [ "name" ] == "Alice" children = json [ "children" ] if children & . size == 1 && name = children . dig ( 0 , "name" ) puts name end else #... end # Finding the parent if parent = json [ "name" ] children = json [ "children" ] if children & . size == 1 && children . dig ( 0 , "name" ) == "Bob" puts parent end else #... end

Thanks to the Ruby &. operator and dig method the code is not necessarily too long and we could even make the conditions a one-liner. We could also split it so that it’s actually a nice piece of code, but that’s not the point. The point is: can we do better with pattern matching?

# Elixir json = ~s({"name": "Alice","age": 30,"children": [{ "name": "Bob", "age": 2 }]}) # Finding the child case Jason . decode ( json ) do { :ok , decoded } -> case decoded do %{ "name" => "Alice" , "children" => [%{ "name" => name }]} -> IO . inspect ( name ) _ -> IO . inspect ( "no match" ) end { :error , % Jason . DecodeError {} = error } -> IO . inspect ( error ) end # Finding the parent case Jason . decode ( json ) do { :ok , decoded } -> case decoded do %{ "name" => parent , "children" => [%{ "name" => "Bob" }]} -> IO . inspect ( parent ) _ -> IO . inspect ( "no match" ) end { :error , % Jason . DecodeError {} = error } -> IO . inspect ( error ) end

We could easily pattern matched on the parsed JSON and directly bind a nested value to a variable. Arrays are no problem either. We could easily combine the case statements to pattern match on various conditions and still end up with very clear and succinct code.

There is no doubt that pattern matching helps us here. So what about Ruby 2.7?

# Ruby 2.7 require 'json' json = << END { "name": "Alice", "age": 30, "children": [{ "name": "Bob", "age": 2 }] } END # Finding the child case JSON . parse ( json , symbolize_names: true ) in { name: "Alice" , children: [{ name: name }]} puts name else # ... end # Finding the parent case JSON . parse ( json , symbolize_names: true ) in { name: parent , children: [{ name: "Bob" }]} puts parent else # ... end

To pattern match on the parsed JSON we must use symbolize keys. Actually, we could have also used Jason.decode(json, keys: :atoms!) to work with Elixir atoms, but this requires the atoms to exist as it calls to :erlang.binary_to_existing_atom . Other than that we use the new case/in syntax for any particular match. To actually match the Elixir code we would need a parse method returning the error rather than throwing an exception. While this is possible in Ruby, most of the Ruby code is designed around catching exceptions.

Nevertheless it’s pretty good, because it still feels rubyish, and helped us to make the code more readable. Here is another example on matching a hash in Ruby 2.7:

# Ruby 2.7 case { a: 0 , b: 1 } in Hash ( a: a , b: 1 ) # do something with a in Object [ a: a ] # do something with a # or in 0 | 1 | 2 # unreachable in { a: 0 , ** rest } if rest . empty? # unreachable # check if it's empty in {} # unreachable end

I would say that working with Ruby pattern matching feels quite similar to Elixir’s. So what’s different though? In Elixir pattern matching is not used just for a more readable case statement. It’s one of the most important control flow technique. Let’s look at the following function for marking an invoice as paid:

# Elixir def mark_invoice_as_paid ( invoice_id , paid_on ) do case DateTime . from_iso8601 ( paid_on ) do { :ok , paid_on , _ } -> case Accounting . pay_invoice ( invoice_id , paid_on ) do % Invoice {} = _struct -> :ok { :error , reason } -> { :error , reason } end _ -> { :error , :invalid_paid_on } end end case mark_invoice_as_paid ( 1 , "1989-12-01" ) do :ok -> IO . inspect "Paid!" { :error , reason } -> IO . inspect "Invoice cannot be paid due to #{ reason } " end

Could we do something like this in Ruby 2.7? Structs could be our result tuples or we could simply got away with using arrays:

# Ruby 2.7 require 'date' Result = Struct . new ( :result , :error ) def parse_date ( date ) Date . parse ( date ) rescue Date :: Error nil end def mark_invoice_as_paid ( invoice_id , paid_on ) case parse_date ( paid_on ) in Date => day # Business logic here Result . new ( :success ) # Or by using atom/array :success in nil Result . new ( :error , :invalid_paid_on ) # Or by using array [ :error , :invalid_paid_on ] end end # Pattern match on result object case mark_invoice_as_paid ( 1 , "1989-12-01" ) in Result => r if r . result == :success puts "Success!" in Result => r if r . result == :error puts r . error end # Pattern match on array case mark_invoice_as_paid ( 1 , "1989-12-01" ) in :success puts "Success!" in :error , error puts error end

As we can see the first problem is that many methods we use in Ruby do not return nil s for unsuccessful results and raises exceptions (similarly to parsing JSON example). We can fix that with a new method that would return nil , :error atom or some kind of result object. In mark_invoice_as_paid definition we can see how we could pattern match on object class and in the usage of mark_invoice_as_paid we can see how we could pattern match on a struct-based result object.

In case of matching on arrays we can omit the parentheses and the final case statement looks pretty good! In reality we can achieve it with objects as well, but we have to implement deconstruct_keys method which is not yet automatically provided in Ruby 2.7. Let’s see how this works when matching the date object. If we want to match on the year for example, we can map the year accessor as follows:

# Ruby 2.7 require 'date' class Date def deconstruct_keys ( keys ) { year: year } end end def find_invoice_by_paid_on ( paid_on ) case parse_date ( paid_on ) in year: .. 1989 # early return of no invoices before year 1989 in year: 1990 .. 1992 # use different database for years 1990 to 1992 in year: 2000 # handle special case for year 2000 in Date => day # fetch invoice based on the day in nil # error end end

I think this looks pretty neat. We could also apply it to our previous struct example, but nevertheless I feel that in places where I reach for returning results objects in Ruby I am pretty happy with my current pattern of:

# Ruby Result = Struct . new ( :result , :resource , :error ) do def success? error . blank? end end def example if true Result . new ( :success , "some kind of result" ) else Result . new ( :failure , nil , "some kind of error" ) end end # And later on result = example () if result . success? puts result . resource else puts result . error end

I use this technique especially where I can chain more things like this together and pass some kind of resources or errors (and certainly not everywhere!). My sentiment is that if statement works absolutely fine here. However, having a more complex scenario than success/error could benefit from a Ruby 2.7 case statement with deconstruct_keys definitions.

Another place where Elixir’s pattern matching shines is function definitions. Look at this:

defmodule DocumentController do use Web , :controller def create ( conn , %{ "data" => %{ "type" => "documents" , "attributes" => document_params }}) do # create document end end

We were able to define a controller function that only matches on correct JSON:API spec format of the JSON sent and directly select the parameters of the document. This is especially great since we can do method overloading in Elixir. In Ruby we don’t have an equivalent.

So what’s the verdict? Pattern matching in Ruby still cannot compete with something designed around pattern matching from the ground up, but it’s great addition to Ruby. It does help with cleaning up code that works with messy hashes and arrays, possibly could help with other objects too, and quite importantly feels rubyish to me. This would be an awesome addition to the language, plus one from me.

Published on 06 January 2020 . Tags: elixir ruby

Any comments? Write me a DM on Twitter.