Ruby Forwardable deep dive

The Forwardable library is one of my favorite tools from Ruby's standard library both for simplifying my own code and learning how to make simple libraries.

I find that the best way to understand how code works is to first understand why it exists and how you use it. In a previous article I wrote about the value of using Forwardable. It takes code like this:

def street
      address.street
    end

    def city
      address.city
    end

    def state
      address.state
    end

And makes it as short as this:

delegate [:street, :city, :state] => :address

Shrinking our code without losing behavior is a great feature which Forwardable provides. So how does it work?

Modules and their context

Forwardable is a module, which can be used to add behavior to an object. Most of the of modules I see tend to be used like this:

class Person
      include SuperSpecial
    end

But Forwardable is different and is designed to be used with the extend method.

require 'forwardable'
    class Person
      extend Forwardable
    end

Using extend includes the module into the singleton_class of the current object. There's a bit more to it than that, but here's a simple model to keep in mind: use include in your class to add instance methods; use extend in your class to add class methods.

Now that we have that out of the way, to use Forwardable, use extend.

Defining forwarding rules

My most often used feature of Forwardable is the one you saw above: delegate. It accepts a hash where the keys can be symbol or string method names, or an array of symbols and strings. The values provided are accessors for the object to which you'll be forwarding the method names.

class Person
      extend Forwardable

      delegate [:message_to_forward, :another_method_name] => :object_to_receive_message,
                :single_method => :other_object
    end

Other shortcuts

Forwardable provides a few methods, and most commonly you'll see their shortened versions: delegate, def_delegator, and def_delegators. These are actually alias methods of the originals.

alias delegate instance_delegate
    alias def_delegators def_instance_delegators
    alias def_delegator def_instance_delegator

The delegate method we reviewed above is a bit of a shortcut for similar behavior that other methods provide in Forwardable.

The def_delegators method accepts multiple arguments but it's sometimes hard for me to remember that one argument in particular is important. The first argument is the reference to the related object, the next arguments are used to create methods to forward.

class SpecialCollection
      extend Forwardable

      def_delegators :@collection, :clear, :first, :push, :shift, :size
      # The above is equivalent to:
      delegate [:clear, :first, :push, :shift, :size] => :@collection
    end

As you can see, with delegate there's a visual separation between the accessor and the list of methods.

There's more of a difference between delegate and def_delegators too.

def instance_delegate(hash) # aliased as delegate
      hash.each{ |methods, accessor|
        methods = [methods] unless methods.respond_to?(:each)
        methods.each{ |method|
          def_instance_delegator(accessor, method)
        }
      }
    end

Here the code loops through the hash argument changing the keys into arrays of methods if they aren't already arrays, and then calls the def_instance_delegator method for each item in the array. Here's what def_instance_delegators looks like. Note that this is the plural version:

def def_instance_delegators(accessor, *methods) # aliased as def_delegators
      methods.delete("__send__")
      methods.delete("__id__")
      for method in methods
        def_instance_delegator(accessor, method)
      end
    end

This method speficially restricts the use of __send__ and __id__ in forwarded messages. These methods are particularly important in communicating with the forwarding object and determining its identity. If you only used delegate and (for some strange reason) you specify either of __send__ or __id__ then those methods will pass right through. That might do exactly what you want or it might introduce some buggy behavior. This is mostly easy to avoid since you'll likely specify all the methods you need.

The different behavior is important to know, however, if you want to do a blanket forward for all methods from another class of objects:

class SpecialCollection
      extend Forwardable

      def_delegators :@collection, *Array.instance_methods
      # The above is equivalent to:
      delegate [*Array.instance_methods] => :@collection
    end

If you do that, you'll likely see warnings from Ruby like this:

warning: redefining `__send__' may cause serious problems

Don't say Ruby didn't warn you!

But def_delegators is a plural version of def_delegator which provides more options than the two we've been reviewing.

class SpecialCollection
      extend Forwardable

      def_delegator :@collection, :clear, :remove
      def_delegator :@collection, :first
    end

The method def_delegator accepts only three arguments. The first is the accessor for the related object (which will receive the forwarded message) and the second is the name of the message to be sent to the related object. The third argument is the name of the method to be created on the current class and is optional; if you don't specify it then the second argument will be used.

Here's what the above def_delegator configurations would look like if you wrote out the feature yourself:

class SpecialCollection
      extend Forwardable

      # def_delegator :@collection, :clear, :remove
      def remove
        @collection.clear
      end

      # def_delegator :@collection, :first
      def first
        @collection.first
      end
    end

You can see how the optional third argument is used as the name of the method on your class (e.g. remove instead of clear).

How the methods are created

We looked at how Forwardable adds class methods to your class. Let's look at the most important one:

def def_instance_delegator(accessor, method, ali = method)
      line_no = __LINE__; str = %{
        def #{ali}(*args, &block)
          begin
            #{accessor}.__send__(:#{method}, *args, &block)
          rescue Exception
            $@.delete_if{|s| Forwardable::FILE_REGEXP =~ s} unless Forwardable::debug
            ::Kernel::raise
          end
        end
      }
      # If it's not a class or module, it's an instance
      begin
        module_eval(str, __FILE__, line_no)
      rescue
        instance_eval(str, __FILE__, line_no)
      end
    end

It looks like a lot, and it is, but let's strip it down to it's simplest form rather than review everything at once. Here's a simpler version:

def def_instance_delegator(accessor, method, ali = method)
      str = %{
        def #{ali}(*args, &block)
          #{accessor}.__send__(:#{method}, *args, &block)
        end
      }
      module_eval(str, __FILE__, __LINE__)
    end

Remembering, of course, that def_instance_delegator is aliased as def_delegator we can see that a string is created which represents what the method definition will be and saved to the str variable. Then then that variable is passed into module_eval.

It's good to know that module_eval is the same as class_eval because I know I often see class_eval but rarely see the other. Regardless, class_eval is merely an alias for module_eval.

The string for the generated method is used by module_eval to create the actual instance method. It evaluates the string and turns it into Ruby code.

Taking this command def_delegator :@collection, :clear, :remove here's what string will be generated:

%{
      def remove(*args, &block)
        @collection.__send__(:clear, *args, &block)
      end
    }

Now it's a bit clearer what's going to be created.

If you're not familiar with __send__, know that it's also aliased as send. If you need to use the send method to match your domain language, you can use it and rely on __send__ for the original behavior. Here, the Forwardable code is cautiously avoiding any clashes with your domain language just in case you do use "send" as a behavior for some object in your system.

Maybe you're scratching your head about what either of those methods are at all. What the heck is send anyway!?

The simplest way to describe it is to show it. This @collection.__send__(:clear, *args, &block) is equivalent to:

@collection.clear(*args, &block)

All Ruby objects accept messages via the __send__ method. It just so happens that you can use the dot notation to send messages too. For any method in your object, you could pass it's name as a string or symbol to __send__ and it would work the same.

It's important to note that using __send__ or send will run private methods as well. If the clear method on @collection is marked as private, the use of __send__ will circumvent that.

The methods defined by Forwardable will accept any arguments as specified by *args. And each method may optionally accept a block as referred to in &block.

It's likely that the acceptance of any arguments and block will not affect your use of the forwarding method, but it's good to know. If you send more arguments than the receiving method accepts, your forwarding method will happily pass them along and your receiving method will raise an ArgumentError.

Managing errors

Forwardable maintains a regular expression that it uses to strip out references to itself in error messages.

FILE_REGEXP = %r"#{Regexp.quote(__FILE__)}"

This creates a regular expression where the current file path as specified by __FILE__ is escaped for characters which might interfere with a regular expression.

That seems a bit useless by itself, but remembering the original implementation of def_instance_delegator we'll see how it's used:

str = %{
      def #{ali}(*args, &block)
        begin
          #{accessor}.__send__(:#{method}, *args, &block)
        rescue Exception
          $@.delete_if{|s| Forwardable::FILE_REGEXP =~ s} unless Forwardable::debug
          ::Kernel::raise
        end
      end
    }

This code recues any exceptions from the forwarded message and removes references to the Forwardable file.

The $@ or "dollar-at" global variable in Ruby refers to the backtrace for the last exception raised. A backtrace is an array of filenames plus their relevant line numbers and other reference information. Forwardable defines these forwarding methods to remove any lines which mention Forwardable itself. When your receive an error, you'll want the error to point to your code, and not the code from the library which generated it.

Looking at this implementation we can also see a reference to Forwardable::debug which when set to a truthy value will not remove the Forwardable lines from the backtrace. Just use Forwardable.debug = true if you run into trouble and want to see the full errors. I've never needed that myself, but at least it's there.

The next thing to do, of course, is to re-raise the cleaned up backtrace. Again Forwardable will be careful to avoid any overrides you may have defined for a method named raise and explicitly uses ::Kernel::raise.

The double colon preceding Kernel tells the Ruby interpreter to search from the top-level namespace for Kernel. That means that if, for some crazy reason, you've defined a Kernel underneath some other module name (such as MyApp::Kernel) then Forwardable will use the standard behavior for raise as defined in Ruby's Kernel and not yours. That makes for predictable behavior.

Applying the generated methods

After creating the strings for the forwarding methods, Forwardable will attempt to use module_eval to define the methods.

# If it's not a class or module, it's an instance
    begin
      module_eval(str, __FILE__, line_no)
    rescue
      instance_eval(str, __FILE__, line_no)
    end

If the use of module_eval raises an error, then it will fallback to instance_eval.

I've yet to find a place where I've needed this instance eval feature, but it's good to know about. What this means is that not only can you extend a class or module with Forwardable, but you can extend an individual object with it too.

object = Object.new
    object.extend(Forwardable)
    object.def_delegator ...

This code works, depending of course on what you put in your def_delegator call.

Forwarding at the class or module level

All these shortcuts for defining methods are great, but they only work for instances of objects.

Fortunately forwardable.rb also provides SingleForwardable, specifically designed for use with modules (classes are modules too).

class Person
      extend Forwardable
      extend SingleForwardable
    end

In the above sample you can see that Person is extended with both Forwardable and SingleForwardable. This means that this class can use shortcuts for forwarding methods for both instances and the class itself.

The reason this library defines those longform methods like def_instance_delegator instead of just def_delegator is for a scenario like this. If you wanted to use def_delegator and those methods were not aliased, you'd need to choose only one part of this library.

class Person
      extend Forwardable
      extend SingleForwardable

      single_delegate [:store_exception] => :ExceptionTracker
      instance_delegate [:street, :city, :state] => :address
    end

As you can probably guess from the above code, the names of each library's methods matter.

alias delegate single_delegate
    alias def_delegators def_single_delegators
    alias def_delegator def_single_delegator

If you use both Forwardable and SingleForwardable, you'll want to avoid the shortened versions like delegate and be more specific by using instance_delegate for Forwardable, or single_delegate for SingleForwardable.

If you liked this article, please join my mailing list at http://clean-ruby.com or pick up the book today!