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