Skip to content

Conversation

@jonathanhefner
Copy link
Contributor

Before this commit, modules included in a DelegateClass could not override delegate methods:

Base = Class.new do
  def foo
    "base"
  end
end

Helper = Module.new do
  def foo
    "helper"
  end
end

WithHelper = DelegateClass(Base) { include Helper }

WithHelper.new(Base.new).foo
# => "base"

This commit defines delegate methods in a separate module, so other modules can come before it in the method lookup chain:

WithHelper.new(Base.new).foo
# => "helper"

Also, because of this change, methods in a DelegateClass block will properly override instead of redefine. Therefore, calling super is faster:

Benchmark script

# frozen_string_literal: true
require "benchmark/ips"
$LOAD_PATH.prepend(".../delegate/lib")
require "delegate"

Base = Class.new do
  def foo
  end
end

Overridden = DelegateClass(Base) do
  def foo
    super
  end
end

overridden = Overridden.new(Base.new)

Benchmark.ips do |x|
  x.report("super") { overridden.foo }
end

Before

Warming up --------------------------------------
               super    75.044k i/100ms
Calculating -------------------------------------
               super    759.506k (± 0.8%) i/s -      3.827M in   5.039488s

After

Warming up --------------------------------------
               super   184.164k i/100ms
Calculating -------------------------------------
               super      1.835M (± 1.0%) i/s -      9.208M in   5.019711s

Fixes https://bugs.ruby-lang.org/issues/19079.

lib/delegate.rb Outdated
@delegate_dc_obj = obj
end
end
klass.include(Module.new do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm always a bit annoyed by anonymous modules. I wonder if it would be worth naming this module.

e.g. klass.const_set("DelegationMethods", Module.new).

The benefit is that it makes it much clearer what is going on when introspecting ancestors or method(:foo).owner.

Of course there is the obvious risk of name clash etc, so maybe not worth it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm always a bit annoyed by anonymous modules.

That's a good point.

Of course there is the obvious risk of name clash etc, so maybe not worth it?

Perhaps a name that deviates from convention would be safe? I've pushed a commit that uses DelegateClass_Methods.

Before this commit, modules included in a `DelegateClass` could not
override delegate methods:

  ```ruby
  Base = Class.new do
    def foo
      "base"
    end
  end

  Helper = Module.new do
    def foo
      "helper"
    end
  end

  WithHelper = DelegateClass(Base) { include Helper }

  WithHelper.new(Base.new).foo
  # => "base"
  ```

This commit defines delegate methods in a separate module, so other
modules can come before it in the method lookup chain:

  ```ruby
  WithHelper.new(Base.new).foo
  # => "helper"
  ```

Also, because of this change, methods in a `DelegateClass` block will
properly override instead of redefine.  Therefore, calling `super` is
faster:

**Benchmark script**

  ```ruby
  # frozen_string_literal: true
  require "benchmark/ips"
  $LOAD_PATH.prepend(".../delegate/lib")
  require "delegate"

  Base = Class.new do
    def foo
    end
  end

  Overridden = DelegateClass(Base) do
    def foo
      super
    end
  end

  overridden = Overridden.new(Base.new)

  Benchmark.ips do |x|
    x.report("super") { overridden.foo }
  end
  ```

**Before**

  ```
  Warming up --------------------------------------
                 super    75.044k i/100ms
  Calculating -------------------------------------
                 super    759.506k (± 0.8%) i/s -      3.827M in   5.039488s
  ```

**After**

  ```
  Warming up --------------------------------------
                 super   184.164k i/100ms
  Calculating -------------------------------------
                 super      1.835M (± 1.0%) i/s -      9.208M in   5.019711s
  ```

Fixes https://bugs.ruby-lang.org/issues/19079.
@jonathanhefner jonathanhefner force-pushed the delegate_class-define-methods-in-module branch from 7caeba1 to 0cb7994 Compare October 24, 2022 16:32
Copy link
Member

@byroot byroot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it. Let's add this to the next dev-meeting.

@byroot
Copy link
Member

byroot commented Oct 25, 2022

@nobu
Copy link
Member

nobu commented Nov 22, 2022

Why not prepend Helper?

@byroot
Copy link
Member

byroot commented Nov 22, 2022

@nobu sure you can do that, but you'd have a similar-ish issue with just defining a method in the delegator:

Foo = Struct.new(:field)
FooDelegator = DelegateClass(Foo) do
  def field
    super.to_s
  end
end

In this example super doesn't call the method that DelegateClass generated, but fallback to Delegator#method_missing, it works but kinda defeat the purpose.

It's somewhat assumed that if you create a delegator, you will want to specialize a few methods.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants