In Ruby, people sometimes make modules called "helpers" that define some useful methods:
module MyHelper
def add(a,b)
a + b
end
end
If you include the module in a class, you can call the helper methods easily without any kind of prefix:
class MyClass
include MyHelper
def join(str1, str2)
add str1, str2
end
end
puts MyClass.new.join("a", "b") # => "ab"
However, I think this has bad semantics. Look at the output below and think about why it is bad:
puts MyClass.new.is_a?(MyHelper) # => true
puts MyClass.new.add(4, 3) # => 7
The fact that MyClass
uses methods from MyHelper
should be an internal implementation detail, but that information is leaking out to the users of MyClass
in all kinds of ways.
The Ruby is_a?
function returns true, telling you that an instance of MyClass
is a MyHelper
. However, if you design a structure like this, it is more likely that your mental model is that an instance uses the helper to accomplish its tasks without being a helper.
There are two more practical problems with this setup. First, people are free to call the helper method #add
on instances of MyClass. That does not make any sense, so we should design our code to disallow that. Second, the #add
method would show up in the list of methods returned by MyClass.new.methods
; calling #methods
in IRB is a super useful way to look for features of an unfamiliar class, and our setup makes that harder because #methods
will return lots of junk. An easy way to fix these two problems is to add a new line that says private
at the top of the module, but I think that is not as good as the solution I propose below.
Refinements
In Ruby 2.0, the language was expanded to include refinements. If you have not heard of refinements, Google it now. However, beware that Module#using
was recently removed so nearly all of the examples you will find are wrong. You can mostly only call using
from the top level of a Ruby file.
Refinements were mainly intended to make monkey-patching safer, but I believe they can also greatly improve the way we use helpers. The Object
class is the parent class of (almost) every Ruby object, so if we refine it with some helper methods, those methods will be available on every object, including self
. This means we can call them easily without any kind of prefix:
module MyHelper
refine Object do
def add(a, b)
a + b
end
end
end
using MyHelper
class MyClass
def join(str1, str2)
add str1, str2
end
end
puts MyClass.new.join("f", "g") # => "fg"
puts MyClass.new.is_a?(MyHelper) # => false
puts MyClass.new.respond_to?(:add) # => false
puts MyClass.new.add(4, 5) # throws NoMethodError if line
# is in a different file
As you can see above, by making a few easy changes to the helper and the code that uses it, we are able to fix all of the semantic problems we were having earlier. This is nice, except for one caveat which I will get to later.
Changing a helper module into a refinement
We had to add two lines of code to MyHelper
to use it as a refinement. What if you do not want to edit the original module? Maybe the module is part of a third-party library or maybe you are not ready to refactor all the code that uses the module. The following works:
module MyRefinement
refine Object do
include MyHelper
end
end
Generally, including a module with include
is equivalent to defining some functions in the same place, and that continues to be true with refinements. This makes me happy.
Making it even better, maybe
Refining Object
is troublesome because the code that uses the helper would be allowed to call helper methods on random objects. Again, this does not make sense so our code should disallow it:
44.add(4, 5) # => 9
Maybe this is OK if you are the only one writing code that uses your helpers, but if you are writing a system or framework that involves helpers, it would be so much nicer to just refine the class that actually uses the helpers. We can do that, as shown below, but it gets a little messy because of the limitations of the Ruby language:
# This method is defined once per project.
def helper(mod, in_class: Object)
Module.new do
refine in_class do
include mod
end
end
end
# This is a helper module.
module MyHelper
def add(a,b)
a + b
end
end
# This is how you write code to use the helper module:
class MyClass; end
using helper MyHelper, in_class: MyClass
class MyClass
def join(str1, str2)
add str1, str2
end
end
puts MyClass.new.join("a", "b") # => "ab"
Unfortunately, I think this is the best we can do if we don't want to refine the Object
class. If anyone—especially Ruby language designers—has an idea of how to clean this up, let me know! I will put all of my failed ideas here.
I think we are very close to having a great new use for refinements, but maybe we need a new feature or two in the language to make it feasible.
Note: refinements are experimental, and the behavior may change in future versions of Ruby! This article was based on ruby 2.0.0dev (2013-01-07 trunk 38733) [x86_64-linux], also known as ruby 2.0.0-rc1.