Skip to content

Instantly share code, notes, and snippets.

@jodosha
Created July 22, 2020 07:37
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jodosha/e3097ed693e9b7c255b658ac39c2e403 to your computer and use it in GitHub Desktop.
Save jodosha/e3097ed693e9b7c255b658ac39c2e403 to your computer and use it in GitHub Desktop.
Ruby Method Overloading
require "benchmark/ips"
require_relative "./method_overloading"
class Foo
include MethodOverloading
def call(number)
"foo #{number}"
end
end
class Bar
def call(number)
"bar #{number}"
end
end
foo = Foo.new
bar = Bar.new
Benchmark.ips do |x|
x.report("method overloading") { foo.call(23) }
x.report("method") { bar.call(23) }
x.compare!
end
__END__
Warming up --------------------------------------
method overloading 24.146k i/100ms
method 305.480k i/100ms
Calculating -------------------------------------
method overloading 225.254k (±13.5%) i/s - 1.111M in 5.053180s
method 3.274M (± 3.5%) i/s - 16.496M in 5.045562s
Comparison:
method: 3273590.9 i/s
method overloading: 225253.9 i/s - 14.53x (± 0.00) slower
require_relative "./method_overloading"
class Foo
include MethodOverloading
def call
puts "foo"
end
def call(number)
puts "foo #{number}"
end
end
foo = Foo.new
foo.call # => "foo"
foo.call(23) # => "foo 23"
module MethodOverloading
def self.included(klass)
klass.class_eval do
@__method_overloading = {}
def self.method_added(method_name)
m = instance_method(method_name)
method_id = [method_name, m.arity]
@__method_overloading[method_id] = m
undef_method method_name
end
def self.respond_to_matching?(method_name, *args)
@__method_overloading.key?([method_name, args.count])
end
def self.matched_call(instance, method_name, *args, &blk)
m = @__method_overloading[[method_name, args.count]]
m.bind_call(instance, *args)
end
end
end
def method_missing(method_name, *args, &blk)
super unless self.class.respond_to_matching?(method_name, *args, &blk)
self.class.matched_call(self, method_name, *args, &blk)
end
def respond_to_missing?(method_name, *)
self.class.respond_to_method?(method_name)
end
end
@splattael
Copy link

Using a string as method_id would make it slightly faster 😁

        method_id = "#{method_name} #{m.arity}"

etc.

👋

@dorianmariecom
Copy link

thanks, didn't know about method_added, undef_method, bind_call, and instance_method

@baweaver
Copy link

Got it down below 9x, will play with other ideas:

require "benchmark/ips"

module MethodOverloading
  def self.included(klass)
    klass.class_eval do
      @_matches = {}
      @_methods = {}
      @_recurse_catch = false

      def self.method_added(method_name)
        if @_recurse_catch
          @_recurse_catch = false
          return
        end

        original_method = instance_method(method_name)

        @_matches[[method_name, original_method.arity]] = original_method
        undef_method method_name

        # Prevent recursive calls to method_added if we're doing it
        # intentionally here.
        @_recurse_catch = true

        # Localize for closure
        matches = @_matches

        if @_methods[method_name]
          define_method(method_name, @_methods[method_name])
        else
          define_method(method_name) do |*as, &fn|
            match = matches[[method_name, as.count]]
            match.bind(self).call(*as, &fn)
          end

          @_methods[method_name] = instance_method(method_name)
        end
      end
    end
  end
end

class Foo
  include MethodOverloading

  def call(number)
    "foo #{number}"
  end

  def call
    "foo 42"
  end
end

class Bar
  def call(number)
    "bar #{number}"
  end
end

foo = Foo.new
bar = Bar.new

Benchmark.ips do |x|
  x.report("method overloading") { foo.call(23) }
  x.report("method") { bar.call(23) }
  x.compare!
end

# Warming up --------------------------------------
#   method overloading    57.941k i/100ms
#               method   252.803k i/100ms
# Calculating -------------------------------------
#   method overloading    641.658k (± 8.7%) i/s -      3.187M in   5.005621s
#               method      5.631M (± 3.4%) i/s -     28.314M in   5.034059s

# Comparison:
#               method:  5631177.7 i/s
#   method overloading:   641657.8 i/s - 8.78x  slower

@baweaver
Copy link

baweaver commented Jul 23, 2020

Got it to 3.3x with a nested hash for matches using hash[method_name][arity]:

require "benchmark/ips"

module MethodOverloading
  def self.included(klass)
    klass.class_eval do
      @_matches = Hash.new { |h, k| h[k] = {} }
      @_methods = {}
      @_recurse_catch = false

      def self.method_added(method_name)
        if @_recurse_catch
          @_recurse_catch = false
          return
        end

        original_method = instance_method(method_name)

        @_matches[method_name][original_method.arity] = original_method
        undef_method method_name

        # Prevent recursive calls to method_added if we're doing it
        # intentionally here.
        @_recurse_catch = true

        # Localize for closure
        method_matches = @_matches[method_name]

        if @_methods[method_name]
          define_method(method_name, @_methods[method_name])
        else
          define_method(method_name) do |*as|
            method_matches[as.size].bind(self).call(*as)
          end

          @_methods[method_name] = instance_method(method_name)
        end
      end
    end
  end
end

class Foo
  include MethodOverloading

  def call(number)
    "foo #{number}"
  end

  def call
    "foo 42"
  end
end

class Bar
  def call(number)
    "bar #{number}"
  end
end

foo = Foo.new
bar = Bar.new

Benchmark.ips do |x|
  x.report("method overloading") { foo.call(23) }
  x.report("method") { bar.call(23) }
  x.compare!
end

# Warming up --------------------------------------
#   method overloading   126.264k i/100ms
#               method   255.798k i/100ms
# Calculating -------------------------------------
#   method overloading      1.711M (± 8.2%) i/s -      8.586M in   5.057014s
#               method      5.697M (± 3.3%) i/s -     28.649M in   5.035081s

# Comparison:
#               method:  5696723.0 i/s
#   method overloading:  1711428.7 i/s - 3.33x  slower

@dblock
Copy link

dblock commented Jul 25, 2020

If anyone wants to continue to improve this, https://github.com/dblock/ruby-overload

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment