Skip to content

Define DelegateClass methods in separate module - #14

Open
jonathanhefner wants to merge 1 commit into
ruby:masterfrom
jonathanhefner:delegate_class-define-methods-in-module
Open

Define DelegateClass methods in separate module#14
jonathanhefner wants to merge 1 commit into
ruby:masterfrom
jonathanhefner:delegate_class-define-methods-in-module

Conversation

@jonathanhefner

Copy link
Copy Markdown
Contributor

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

Base=Class.newdodeffoo"base"endendHelper=Module.newdodeffoo"helper"endendWithHelper=DelegateClass(Base){includeHelper}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: truerequire"benchmark/ips"
$LOAD_PATH.prepend(".../delegate/lib")require"delegate"Base=Class.newdodeffooendendOverridden=DelegateClass(Base)dodeffoosuperendendoverridden=Overridden.new(Base.new)Benchmark.ipsdo |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.

Comment threadlib/delegate.rb Outdated
@delegate_dc_obj = obj
end
end
klass.include(Module.new do

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
jonathanhefnerforce-pushed the delegate_class-define-methods-in-module branch from 7caeba1 to 0cb7994CompareOctober 24, 2022 16:32

@byrootbyroot left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
Member

@nobu

nobu commented Nov 22, 2022

Copy link
Copy Markdown
Member

Why not prepend Helper?

@byroot

Copy link
Copy Markdown
Member

@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 freeto 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

@jonathanhefner@byroot@nobu