So what’s the deal with Ruby refinements, anyway?

If you’ve been following Ruby developments for the past couple of years, chances are you’ve heard about refinements. You may have heard that they are controversial, confusing, or even “broken”.

It’s true that refinements had some growing pains in their early, experimental versions. But having spent some time exploring the feature as it now exists in Ruby 2.1 and onward, I think they’re a valuable addition to the language. In a language where any class can be re-opened at any time, refinements provide a nice mix of language malleability tempered by locality and visibility.

Back in October I released a video to my RubyTapas subscribers which set out to introduce and demystify this powerful new ability. Today I’m releasing it for free in hopes that it will help clear up some of the confusion around refinements.

Here’s the video:

 

By the way, if you want to see an example of a real-world use of refinements, check out the Sequel core_refinements library. Sequel has a set of core class extensions which are traditionally added globally. But with refinements, it’s possible to make use of these convenience extensions without either forcing or requiring them to be globally available. This is particularly handy within gems, where we might want the ease of use of the extension methods, but we don’t want to force global extensions on our library clients.

If you prefer reading over watching, the script is below. And remember, if you like this, please consider subscribing and supporting the creation of more videos like this!


 

In the last episode (#249), we developed some code for unindenting text inside of heredocs.

This method seems like a perfect candidate to be made into an extension to the String class. That way instead of wrapping the heredoc in a call to unindent() , we could append it as a message send instead.

So let’s go ahead and do that. We reopen the String class and define unindent() inside it. References to the method’s argument become implicit references to self instead.

Now, if you’ve watched episode #226, a little alarm bell might be going off in your head right now. In that episode I talked about how even when we add brand new methods to core classes, these methods are conflicts waiting to happen. This is doubly true of methods like this one: it is not only possible, but likely, that someone else will have the idea to add an #unindent method to String. And, in fact, I know of at least one Rubygem which adds exactly this method to String. If our implementation of #unindent differs slightly from the conflicting definition, then whichever one “wins” based on the program’s load order will cause subtle and difficult-to-track-down bugs in the code expecting different semantics.

“But Avdi!” you might object. “I just won’t include libraries that include conflicting definitions in my project!” The difficulty comes when some unrelated gem you need—for instance, an API wrapper around some remote service—has an implicit dependency on a gem that extends a core class with a conflicting method definition.

How can we be sure that our extension to String is the only one our code will use, while also ensuring that our extensions won’t interfere with third-party code? This is a question which has vexed Ruby programmers for many years. And it has lead some of us to come to the conclusion that extensions to core classes—or any classes we don’t ourselves own—are not worth the cost.

However, Ruby 2.0 introduced an experimental answer to this question: a feature called “refinements”. In Ruby 2.1 it ceased to be experimental. In a nutshell, refinements are a way to limit the scope of an extension to a class to only the code we control.

Let’s convert our extension to a refinement. But first, let’s create a conflicting String extension. This definition won’t actually unindent anything; it’ll just return an obvious flag value to tell us when we are invoking the conflicting definition.

Moving on, we create a new module to contain our refinements, calling it StringRefinements. Then we move our extension—including the reopened String class—inside this module. Then we switch the String class definition into a refine declaration, including a do keyword.

At this point, we’ve declared our refinement, but it hasn’t taken effect anywhere. Inside our Wonderland module, we add a using declaration, with the name of our refinements module as an argument. This tells Ruby that inside the Wonderland module, the refinments we defined should take effect. Remember that inside this module we call String#unindent, and we are hoping it will invoke our version of unindent.

Outside of the Wonderland module, we use String#unindent on another heredoc and assign the result to a constant.

With all that done, we then output the contents of our unindented string constant, followed by the contents of the second heredoc. The output tells the story: inside the Wonderland module, the refinements defined within the StringRefinements module took precedence. But outside that module, the global definition of String#unindent was in effect. Our string class extensions are no longer in conflict.

It is important to understand that the effect of a using statement is strictly lexically scoped. To see what this means, let’s reopen the Wonderland module and define another unindented string constant. Note that this time, we do not declare that we are using StringRefinements.

When we output the contents of the string, we can see that the unrefined definition of String#unindent was in effect. This is despite the fact that we declared we were using Stringrefinements in another definition of this same module. What this tells us is that declaring that we are using a refinement module in one location does not “infect” other code defined inside the same module. Just like local variables, the refinements are in effect only up to the end of the module block in which they are used.

And this is a very good thing. Refinements exist to address some of the confusing and surprising consequences of being able to extend any class at any time. The fact that refiements are strictly lexical means we cannot change the behavior of other code “at a distance”. Anywhere that a a refinement is in effect, we will be able to scroll the file up in our editor and see that the refinement is in effect.

And that’s it for today. Happy hacking!

Like what you see? This is just a taste of RubyTapas! Sign up today to get two videos a week, along with full source code and transcripts. Or click here to learn more.