Episode #250: Refinements

Upgrade to download episode video.

Episode Script

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

def unindent(s)
  s.gsub(/^#{s.scan(/^[ \t]+(?=\S)/).min}/, "")
end

module Wonderland
  JABBERWOCKY = unindent(<<-EOF)
      'Twas brillig, and the slithy toves
      Did gyre and gimble in the wabe;
      All mimsy were the borogoves,
      And the mome raths outgrabe.

    -- From "jabberwocky", by Lewis Carroll
  EOF
end

puts Wonderland::JABBERWOCKY

# >>   'Twas brillig, and the slithy toves
# >>   Did gyre and gimble in the wabe;
# >>   All mimsy were the borogoves,
# >>   And the mome raths outgrabe.
# >>
# >> -- From "jabberwocky", by Lewis Carroll

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.

JABBERWOCKY = <<-EOF.unindent
# ...

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.

class String
  def unindent
    gsub(/^#{scan(/^[ \t]+(?=\S)/).min}/, "")
  end
end

module Wonderland
  JABBERWOCKY = <<-EOF.unindent
      'Twas brillig, and the slithy toves
      Did gyre and gimble in the wabe;
      All mimsy were the borogoves,
      And the mome raths outgrabe.

    -- From "jabberwocky", by Lewis Carroll
  EOF
end

puts Wonderland::JABBERWOCKY

# >>   'Twas brillig, and the slithy toves
# >>   Did gyre and gimble in the wabe;
# >>   All mimsy were the borogoves,
# >>   And the mome raths outgrabe.
# >>
# >> -- From "jabberwocky", by Lewis Carroll

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.

class String
  def unindent
    "<CONFLICTING UNINDENT OUTPUT>"
  end
end

module StringRefinements
  refine String do
    def unindent
      gsub(/^#{scan(/^[ \t]+(?=\S)/).min}/, "")
    end
  end
end

module Wonderland
  using StringRefinements
  JABBERWOCKY = <<-EOF.unindent
      'Twas brillig, and the slithy toves
      Did gyre and gimble in the wabe;
      All mimsy were the borogoves,
      And the mome raths outgrabe.

    -- From "jabberwocky", by Lewis Carroll
  EOF
end

UNREFINED =<<EOF.unindent
Yadda yadda yadda
EOF

puts "Refined:"
puts Wonderland::JABBERWOCKY
puts "Unrefined:"
puts UNREFINED

# >> Refined:
# >>   'Twas brillig, and the slithy toves
# >>   Did gyre and gimble in the wabe;
# >>   All mimsy were the borogoves,
# >>   And the mome raths outgrabe.
# >>
# >> -- From "jabberwocky", by Lewis Carroll
# >> Unrefined:
# >> <CONFLICTING UNINDENT OUTPUT>

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.

class String
  def unindent
    "<CONFLICTING UNINDENT OUTPUT>"
  end
end

module StringRefinements
  refine String do
    def unindent
      gsub(/^#{scan(/^[ \t]+(?=\S)/).min}/, "")
    end
  end
end

module Wonderland
  using StringRefinements
  JABBERWOCKY = <<-EOF.unindent
      'Twas brillig, and the slithy toves
      Did gyre and gimble in the wabe;
      All mimsy were the borogoves,
      And the mome raths outgrabe.

    -- From "jabberwocky", by Lewis Carroll
  EOF
end

module Wonderland
  TWINKLE = <<-EOF.unindent
    Twinkle, twinle little bat...
  EOF
end

puts Wonderland::TWINKLE

# >> <CONFLICTING UNINDENT OUTPUT>

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 refinements 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!