Sunday, January 13, 2013

Ruby 2.0: Helpers should be refinements

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.

1 comment:

  1. Actually, I think I just discovered that there is a second big problem with using refinements as helpers, which you will probably discover as soon as you start trying to write real code. Does anyone have any guesses what the second problem is?

    ReplyDelete

Note: Only a member of this blog may post a comment.