Question about BlankSlate.reveal


#1

Hi folks,

I’ve been working on a chapter for “Ruby Best Practices”[0] about the
dynamic nature of Ruby, and I initially thought Jim W.'s
BlankSlate would be an excellent example (even though 1.9 has
BasicObject).
However, when I look at the reveal class method, I found what seems to
be a fairly major limitation that I am having a hard time glossing
over in my prose.

The issue is that when you reveal a previously hidden method, the
UnboundMethod that was stored gets bound to whatever the first
instance is that calls the revealed method, because Jim’s code caches
the bound_method via a closure. The best ‘fix’ I got was to drop the
caching and rebind the method to self on every single method call, but
I wasn’t able to get away with that without saying “This is probably a
performance nightmare”. Does anyone have a better suggested fix? The
original code for reveal, pulled from the builder gem, is below.

-greg

[0] http://rubybestpractices.com

Code pulled from blankslate.rb in the builder gem.

# Redefine a previously hidden method so that it may be called on a 

blank
# slate object.
def reveal(name)
bound_method = nil
unbound_method = find_hidden_method(name)
fail “Don’t know how to reveal method ‘#{name}’” unless
unbound_method
define_method(name) do |*args|
bound_method ||= unbound_method.bind(self)
bound_method.call(*args)
end
end
end


#2

2009/1/25 Gregory B. removed_email_address@domain.invalid:

The issue is that when you reveal a previously hidden method, the
UnboundMethod that was stored gets bound to whatever the first
instance is that calls the revealed method, because Jim’s code caches
the bound_method via a closure. The best ‘fix’ I got was to drop the
caching and rebind the method to self on every single method call, but
I wasn’t able to get away with that without saying “This is probably a
performance nightmare”. Does anyone have a better suggested fix? The
original code for reveal, pulled from the builder gem, is below.

From what you write caching a bound method seems a bad idea because it
is instance specific. Caching of an unbound method would seem much
more reasonable (unless, that is, I am missing something).

On the other hand:

 define_method(name) do |*args|
   bound_method ||= unbound_method.bind(self)
   bound_method.call(*args)
 end

end
end

I do not really see caching that extends the instance on which #reveal
was invoked. There is actually one closure per instance (more
correctly: one closure per call of reveal) and so no global cache.

Where exactly is your problem?

Cheers

robert


#3

On Mon, Jan 26, 2009 at 8:55 AM, Robert K.
removed_email_address@domain.invalid wrote:

I do not really see caching that extends the instance on which #reveal
was invoked. There is actually one closure per instance (more
correctly: one closure per call of reveal) and so no global cache.

Where exactly is your problem?

I think you missed that the local variable was at the class level.

class A
a = nil
define_method :foo do
?> a ||= self

end
end
=> #Proc:0x000585d4@:11(irb)

A.new.foo
=> #<A:0x57030>

A.new.foo
=> #<A:0x57030>

A.new.foo
=> #<A:0x57030>

A.new.foo
=> #<A:0x57030>

But I think I have a suitable fix:
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/325826

What do you think?

-greg


#4

On 26.01.2009 17:18, Gregory B. wrote:

On Mon, Jan 26, 2009 at 8:55 AM, Robert K.
removed_email_address@domain.invalid wrote:

I do not really see caching that extends the instance on which #reveal
was invoked. There is actually one closure per instance (more
correctly: one closure per call of reveal) and so no global cache.

Where exactly is your problem?

I think you missed that the local variable was at the class level.

Right, you stated so in the text and then I got confused by
“unbound_method.bind(self)”. :slight_smile:

But in that case my remark about binding at class level remains true:
there is no point in binding to a particular instance at class level and
caching that. And your code seems to indicate that you agree with me
there.

But I think I have a suitable fix:
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/325826

What do you think?

Looks good to me. I didn’t test it though. :wink:

Kind regards

robert


#5

On Tue, 27 Jan 2009, Robert K. wrote:

I think you missed that the local variable was at the class level.

Right, you stated so in the text and then I got confused by
“unbound_method.bind(self)”. :slight_smile:

Sorry – I posted my reply to your earlier post without having noticed
this post.

David


David A. Black / Ruby Power and Light, LLC
Ruby/Rails consulting & training: http://www.rubypal.com
Coming in 2009: The Well-Grounded Rubyist (http://manning.com/black2)

http://www.wishsight.com => Independent, social wishlist management!


#6

Hi –

On Mon, 26 Jan 2009, Robert K. wrote:

is instance specific. Caching of an unbound method would seem much

def reveal(name)
I do not really see caching that extends the instance on which #reveal
was invoked. There is actually one closure per instance (more
correctly: one closure per call of reveal) and so no global cache.

Where exactly is your problem?

Let’s say you do BasicSlate.reveal(:meth). Then you do slate.meth. At
that point, bound_method in the closure is the meth method, bound to
the object slate. When you then call other_slate.meth, the ||=
short-circuits, and bound_method is still the unbound method bound
to slate (not to other_slat).

So every time you call meth, you’re calling it on the same instance.

David


David A. Black / Ruby Power and Light, LLC
Ruby/Rails consulting & training: http://www.rubypal.com
Coming in 2009: The Well-Grounded Rubyist (http://manning.com/black2)

http://www.wishsight.com => Independent, social wishlist management!


#7

On Mon, Jan 26, 2009 at 4:07 PM, Robert K.


#8

On Mon, Jan 26, 2009 at 4:07 PM, Robert K.
removed_email_address@domain.invalid wrote:

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/325826

What do you think?

Looks good to me. I didn’t test it though. :wink:

Cool. This is what I’m going to go with in my chapter. I emailed Jim
to see if I can get a definitive ‘yes, this is a) a bug or b) some
special casing for builder’s needs or c) some special sauce that you
missed’. My guess is it’s a), but stranger things have happened :slight_smile:

-greg