A quest for pattern-matching in Ruby
by Paweł Świątkowski
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.
So, why I miss it so much? Take for example this snippet from Elixir’s docs:
Ruby equivalent would go like this:
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.
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.
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
:error. Then we can do:
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.
Now let me explain what is happening here. The left side (key of the hash) is an array with two values, for example
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:
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.