One thing new Ruby users are sometimes surprised by is Ruby’s lack of overloaded methods. Once you get used to duck typing you quickly realize why this is a strength for Ruby rather than a weakness. Nonetheless I have often wondered what it would look like to implement something resembling method overloading in Ruby. To find out, I put together the hack below allowing overloaded methods to be defined on a class. It allows for adding of new overloaded definitions in a single class, but currently doesn’t have very good behavior when dealing with subclasses or modules. To reiterate, I don’t think this is a good idea, something Ruby should support, or that you should actually use the code below. I hope you find it interesting though.

the spec:

  require 'method_overloading'

  describe "Method overloading" do
    it "should allow methods to be invoked" do
      klass = Class.new do
        overloading :my_method do
          _(Fixnum) {|n| n*2}
        end
      end

      klass.new.my_method(3).should == 6
    end

    it "should maintain receiver as self within method block" do
      klass = Class.new do
        overloading :my_method do
          _(Object) {|n| self}
        end
      end

      instance = klass.new
      instance.my_method(3).should equal(instance)
    end

    it "should raise error if no matching implementation can be found" do
      klass = Class.new do
        overloading :my_method do
          _(Fixnum) {|n| n}
        end
      end

      lambda {
        klass.new.my_method("String")
      }.should raise_error(NoMethodError, "No implementation found for my_method(String)")
    end

    it "should invoke different implementations based on argument type" do
      klass = Class.new do
        overloading :my_method do
          _(Fixnum) {|n| n*2}
          _(String) {|s| s.upcase}
        end
      end

      klass.new.my_method(3).should == 6
      klass.new.my_method("A String").should == "A STRING" 
    end

    it "should invoke superclass implementations for arguments in type of subclass" do
      klass = Class.new do
        overloading :my_method do
          _(Numeric) {|n| n * 2}
        end
      end

      klass.new.my_method(3).should == 6
      klass.new.my_method(3.0).should == 6.0
    end

    it "should invoke implementation that most closely matches argument type" do
      klass = Class.new do
        overloading :my_method do
          _(Numeric) {|n| n * 2}
          _(Integer) {|n| n * 3}
          _(Object)  {|n| n * 4}
        end
      end

      klass.new.my_method(3).should == 9
    end

    it "should allow reopening to change a single overloaded method" do
      klass = Class.new do
        overloading :my_method do
          _(String) {|n| "original string implementation"}
          _(Fixnum) {|n| "original fixnum implementation"}
        end
      end

      klass.class_eval do
        overloading :my_method do
          _(String) {|n| "new string implementation"}
        end
      end

      klass.new.my_method("a").should == "new string implementation" 
      klass.new.my_method(1).should == "original fixnum implementation" 
    end

    it "should allow multiple methods to be overloaded with separate implementations" do
      klass = Class.new do
        overloading :method_1 do
          _(String) {|n| "method_1 string implementation"}
        end

        overloading :method_2 do
          _(String) {|n| "method_2 string implementation"}
        end
      end

      klass.new.method_1("a").should == "method_1 string implementation" 
      klass.new.method_2("a").should == "method_2 string implementation" 
    end
  end

the implementation:

  class Class
    def overloading_definition_context(method_name)
      @definition_contexts ||= {}
      @definition_contexts[method_name] ||= OverloadingDefinitionContext.new(self, method_name)
    end

    def overloading(method_name, &block)
      overloading_definition_context(method_name).instance_eval &block
      define_method(method_name) do |arg|
        send(self.class.overloading_definition_context(method_name)[arg.class], 
             arg)
      end
    end

    class OverloadingDefinitionContext
      attr_accessor :implementation_block

      def initialize(klass, method_name)
        @klass = klass
        @method_name = method_name
        @implementations = {}
      end

      def _(arg_type, &block)
        method_name = "__#{@method_name}_#{arg_type.object_id.abs}" 
        @klass.send(:define_method, method_name, &block)
        @implementations[arg_type] = method_name
      end

      def [](target_arg_type)
        @implementations[implementation_arg_type(target_arg_type)]
      end

      def implementation_arg_type(target_arg_type)
        possible_arg_types = @implementations.keys.select do |arg_type| 
          target_arg_type <= arg_type
        end

        if possible_arg_types.empty?
          raise NoMethodError.new("No implementation found for #{@method_name}(#{target_arg_type})")
        end

        possible_arg_types.sort.first
      end
    end
  end

Sorry, comments are closed for this article.