Ruby Voodoo

Deep dives into random corners of my favorite programming language.

9

OCT
2008

Dual Interface Modules

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 its 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 method
# ~>      `hello' 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.

In: Ruby Voodoo | Tags: APIs | 2 Comments
Comments (2)
  1. Dan
    Dan October 23rd, 2011 Reply Link

    After watching Rich Hickey's talk on simplicity vs. ease i've been looking for ways to back off creating objects and using more module for namespaces.

    This works perfectly!

    Where are all these functions documented?

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
    2. James Edward Gray II
      James Edward Gray II October 23rd, 2011 Reply Link

      If you meant module_function and extend, see:

      ri Module#module_function
      ri Object#extend
      

      If you meant where do module functions get documented, you can use Kernel as an example:

      ri Kernel
      
      1. Reply (using GitHub Flavored Markdown)

        Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

        Ajax loader
Leave a Comment (using GitHub Flavored Markdown)

Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

Ajax loader