A quest for pattern-matching in Ruby

by Paweł Świątkowski
13 Feb 2016

We all know that functional programming is cool, nowadays. Many Ruby developers I know had some brief affairs with Elixir, Scala or Clojure. And it’s definitely a good thing. I’ve been writing some Elixir lately too (namely, a simple stress-tester for HTTP servers I called Locust). One very common thing is to miss some aspects of functional programming when you go back to coding in Ruby. If you did not experience it, it’s similar to missing Ruby’s auto-return when you switch to JavaScript.

On the other hand, when you take any high-level description of Ruby language, you are going to read that it’s mostly object-oriented, but also takes a lot from functional world. It’s true, in a sense. For example prevailing usage of .map and less common use of .reduce instead of for is one of them. But can we make it even more functional-oriented?

This post is a report of my attempts to do so. I created a project called Noaidi, which tries to address what I miss most - pattern matching. Or at least some useful parts of it. The project is really experimental, so please don’t rely on it in any way, but I welcome any number of comments or discussion.

Why pattern-matching?

So, why I miss it so much? Take for example this snippet from Elixir’s docs:

case File.read(file) do
  {:ok, body}      -> # do something with the `body`
  {:error, reason} -> # handle the error caused by `reason`
end

Ruby equivalent would go like this:

begin
  f = File.open(path)
  body = f.read
  # do something with file
rescue
  error = $!
  # handle failure
end

It’s obviously longer and, at least for me, feels less elegant. We are also using exceptions for control flow, which is not a recommended Ruby-way, but we are using it in Elixir too (which is, in turn, recommended with Erlang’s philosophy of crashing fast and often). But the real danger, of course, is in catch-all rescue statement. When something bad happens while we process the file, interpreter will end up in the rescue statement designed to handle failure in opening the file, not processing it.

Now, we could, of course, rescue specifically from Errno::ENOENT, which is most likely to be the exception, but imagine a situation when the universe of possible errors is huge and listing them is impossible. So, how can we rewrite the Ruby snippet to do exactly what we want it to do (without using File.exists? etc., just using general-purpose rescue)? Let’s try.

op_result = begin
  File.open(path)
rescue
  $!
end

if op_result.is_a?(File)
  body = op_result.read
  # do something with file
elsif op_result.is_a?(Exception)
  # handle failure
end

Well… Shit hit the fan. There’s really a lot of bad things happening here and I’m not even going to enumerate them, but basically if you ever see if x.is_a?(Something) - run for your life.

Enter Noaidi

The goal of the project, previously named ErlMod, is to address issues I mentioned above by allowing to use pattern matching. The examples below show what I have so far. Let’s assume we have a open_file method, which returns array of length 2 with first element being a symbol :ok or :error. Then we can do:

Noaidi.match open_file(path), {
  [:ok, File]         => ->(file) { process_file(file) },
  [:error, Exception] => ->(ex) { handle_file_opening_exception(path, ex) }
}

This is as far as Ruby syntax let me get. It is basically just a method accepting two parameters - a value and a hash of possible matches - but looks close enough to its Elixir counterpart. match keyword was taken from Rust.

Obviously it’s not possible to go much further - I would definitely like to put variable name on the left side and have it accessible in the block on the right side, but it seems impossible to achieve in Ruby. I also don’t like => ->() part, but I can live with that. If you can’t, it is possible to use lambda keyword and old syntax, which look a bit better but less concise.

Noaidi.match open_file(path), {
  [:ok, File]         => lambda {|file| process_file(file) },
  [:error, Exception] => lambda {|ex| handle_file_opening_exception(path, ex) }
}

Now let me explain what is happening here. The left side (key of the hash) is an array with two values, for example :ok and File. This is a definition of the pattern, which is then transformed into a matcher objects, which are tested against the actual value. For now it works exactly like case statements so those two are compared using === operator, but this might be a subject to change - for example I would like to include partial hash matching, like RSpec does. This means that open_file has to return a symbol :ok and any instance of File class (or its descendant, if exists) in order to have a match. The length of array has to match too.

If we find a match, the provided block is executed. You probably noticed that not all left side is passed as arguments. The mechanism here is supposed to be smart: if you have exact match (like symbol to symbol or number to number) you probably don’t need to use it on the right side and this is why it’s skipped. So if you pattern is [:ok, 200, String, Hash], the right side will get two arguments - a string and a hash.

The result of running the right side is, of course, returned as a result of whole Noaidi.match. If no match is found, exception is raised. First found match is always used.

The downside of it is probably performance. If you run Noaidi.match in a frequently-called method, every time a new matching mechanism is instantiated, which takes some time and resources. Right now there is no way to memoize the match and test it against subsequent values. However, to overcome this problem, you may use another feature of Noaidi - the module.

Lightweight modules are what the projects started with. They are (I think) quite well described in the README of the project, so I’m not going to repeat it here. But we can rewrite our pattern matching above to use modules too:

file_handler = Noaidi.module do
  fun :handle, [:ok, File] {|_, file| process_file(file) }
  fun :handle, [:error, Exception] {|_, ex| handle_file_opening_exception(ex) }
end

file_handler.handle(open_file(path))

This way you can keep the module memoized, separated and call right handling function every time you need without the overhead. I have to admit that in a way I think of it as a recommended way to do it.

Yet again, this is just a proof-of-concept that we can have some goodies of functional programming in Ruby without losing all the good things the language has. We just need right tools for that and I was a bit surprised that no one tried it before (at least I don’t know about such attempts). By the way, this is not all that Noaidi gives you. Check the project’s README for return contracts too.

end of the article

Tags: ruby

This article was written by me – Paweł Świątkowski – on 13 Feb 2016. I'm on Fediverse (Ruby-flavoured account, Elixir-flavoured account) and also kinda on Twitter. Let's talk.

Related posts: