Dual Interface Modules

Posted 2 months ago in Ruby Tutorials and Ruby Voodoo.

I'm guessing we've all seen Ruby's Math Module. I'm sure you know that you can call methods in it as "module (or class) methods:"

Math.sqrt(4)  # => 2.0

That's just one way to use the Math Module though. Another is to treat it as a mixin and call the same methods as instance methods:

module MyMathyThing
  extend Math

  def self.my_sqrt(*args)
    sqrt(*args)
  end
end

MyMathyThing.my_sqrt(4)  # => 2.0

Ruby ships with a few Modules that work like this, including the mighty Kernel.

How is this dual interface accomplished? With the seldom seen module_function() method. You use this much like you would private(), to affect all following method definitions:

module Greeter
  module_function

  def hello
    "Hello!"
  end
end

module MyGreeter
  extend Greeter

  def self.my_hello
    hello
  end
end

Greeter.hello       # => "Hello!"
MyGreeter.my_hello  # => "Hello!"

As you can see, it magically gives us the dual interface for the methods beneath it. You can also affect specific methods by name, just as you could with private(). This is equivalent to my definition above:

module Greeter
  def hello
    "Hello!"
  end
  module_function :hello
end

What this helper actually does is to make a copy of the method and move it up to the module interface level. Once the copy is made, they can be affected separately:

module Copies
  def copy
    "Copied!"
  end
  module_function :copy

  alias_method :copier, :copy
  public       :copier
  undef        :copy
end

Copies.copy  # => "Copied!"
c = Object.new.extend(Copies)
c.copier     # => "Copied!"
c.copy
# ~> -:16: undefined method `copy' for #<Object:0x26e6c> (NoMethodError)

This process also marks the instance method version private(), which is why I needed the call to public() in the last example. This means that methods you mixin to another object do not add to it's external interface:

module Selfish
  module_function

  def mine
    "mine"
  end
end

module Sharing
  extend Selfish

  def self.yours_and_mine
    "yours and #{mine}"
  end
end

Sharing.yours_and_mine  # => "yours and mine"
Sharing.mine
# ~> -:18: private method `mine' called for Sharing:Module (NoMethodError)

As we've seen there are some advantages to this interface, but there are some drawbacks too. For example, I first tried to write the MyGreeter example as:

module MyGreeter include Greeter

 module_function

 def my_hello
   hello
 end

end

MyGreeter.my_hello # => # ~> -:15:in my_hello': undefined local variable or methodhello' for MyGreeter:Module (NameError) # ~> from -:20

That didn't work because the Greeter functionality was not copied up with my method. You can fix that by using a different trick to duplicate the functionality:

module MyGreeter
  include Greeter

  extend self  # mixin functionality to our own Module interface

  def my_hello
    hello
  end
end

MyGreeter.my_hello  # => "Hello!"

This provides a similar dual interface, but there are important differences. First, we've changed the ancestors() of MyGreeter, not copied methods:

MyGreeter.ancestors  # => [MyGreeter, Greeter]

There's just one method and changing it affects everywhere it is used.

We also didn't magically make the mixin interface private() and it will bleed through:

module MyNestedGreeter
  extend MyGreeter

  def self.my_nested_hello
    my_hello
  end
end

MyNestedGreeter.my_nested_hello  # => "Hello!"
MyNestedGreeter.my_hello         # => "Hello!"

It's all tradeoffs of course, but knowing our options allows us to make informed choices about what is best for our needs.

Add Your Thoughts

You can use Markdown in the body of your comment to format text and make links.

Note that I reserve the right to edit any content you post here. I typically exercise this right to fix formatting issues. All posts must be approved so spam will never be seen on these pages.

Author:
URL or Email (optional):
Body: