Undo Object.extend

Hi all:

I’m trying to change the behaviour of a specific instance at runtime.
I’m using modules in the following way:

module NormalBehaviour
def talk
puts ‘Hi’
end
end

class A
include NormalBehaviour
end

module EnhancedBehaviour
def talk
super
puts ‘Goodbye’
end
end

a = A.new
a.talk

Output: Hi

a.extend EnhancedBehaviour
a.talk

Output: Hi

Goodbye

I would like to be able to make variable “a” to its original state,
i.e., by ‘deincluding’ EnhancedBehaviour. Something like this:
a.deinclude EnhancedBehaviour
a.talk

Output: Hi

Is there a way to do this?

On Mon, Sep 1, 2008 at 3:39 PM, José Ignacio
[email protected] wrote:
Maybe you want to use pushable behavior

class Behavior
attr_reader :block
def initialize &blk
@block = blk
end
end

module Kernel
def Behavior &blk
Behavior::new &blk
end
end

class Module
def empty?; instance_methods.empty? end
def empty!; instance_methods.each do |im| remove_method im end end
end

module Pushable
CannotPopException = Class::new RuntimeError
ArgumentError = Class::new ::ArgumentError

def pop_behavior
@bsp -= 1
raise CannotPopException, “empty entity #{self}” if @bsp < 0
@behavior_stack[@bsp].empty!
end

def push_behavior *behaviors, &blk
@behavior_stack ||= []
@bsp ||= 0
raise ArgumentError,
“push_behavior takes at least one behavior or block” if
behaviors.empty? and blk.nil?

behaviors.each do |behavior|
_push_behavior behavior
end

_push_behavior blk if blk
self
end

private

def _push_behavior behavior
m = @behavior_stack[@bsp]
@behavior_stack << ( m = Module::new ) unless m
include m rescue extend m # this is autoprotected against double
inclusion; so finally turns out it is a feature :wink:
m.empty!
m.module_eval &(behavior.block rescue behavior)
@bsp += 1
end
end # module Pushable

class << Pushable
def new *args, &blk
c = Class::new( *args, &blk )
c.extend self
c
end
end # class << Pushable

class << PushableModule = Module::new
def new *args, &blk
m = Module::new( *args, &blk )
m.extend Pushable
m
end
end
------------------------------------ 8<

See these test cases to learn more about how to use this:

o = Object.new
o.extend Pushable
b = Behavior do
  def a; 42 end
end
o.push_behavior b
assert_equal 42, o.a
o.pop_behavior
assert_raise NoMethodError do o.a end

HTH
Robert


C’est véritablement utile puisque c’est joli.

Antoine de Saint Exupéry

Thanks, that’s a really nice piece of code that helps a lot :slight_smile:

Why use a @bsp variable instead of using @behavior_stack.pop or
@behavior_stack.last ?

It seems that the trick to undo Object.extend is done by erasing all
instance methods in the modules. Isn’t this a bit strange? If Ruby
allows including modules in an instance’s ancestor chain, there should
be a way to remove them…

On Mon, Sep 1, 2008 at 10:34 PM, José Ignacio
[email protected] wrote:

Thanks, that’s a really nice piece of code that helps a lot :slight_smile:

Why use a @bsp variable instead of using @behavior_stack.pop or
@behavior_stack.last ?
Good question ;). Actually I am leaving empty modules on the stack
that can be reused, thus
after a push_behavior the stack might look like this [ x, module with
the methods from pushed behavior ] and the sp is 1
after pop_behavior the situation is as follows stack : [x, empty
module] and the sp=0.
This avoids unnecessary recreation of modules ( and lets the GC rest,
but that was not the idea :wink:

It seems that the trick to undo Object.extend is done by erasing all
instance methods in the modules. Isn’t this a bit strange? If Ruby
allows including modules in an instance’s ancestor chain, there should
be a way to remove them…
I am not sure to understand this question, do you mean removing them
from the includee?
That would not work, sorry if I miss the obvious here maybe you might
elaborate.

Cheers
Robert


Posted via http://www.ruby-forum.com/.


C’est véritablement utile puisque c’est joli.

Antoine de Saint Exupéry

On Tue, Sep 2, 2008 at 12:54 PM, José Ignacio
[email protected] wrote:

a.extend EnhancedBehaviour
a.extend EnhancedBehaviour # Nothing happens
That might be a feature or a bug, when pushing behavior dynamically
depending on some events it might be better to push it twice but I am
sure you implemented it how you needed it.

Trouble is that I don’t really know what happens in the background in
Ruby when you use Object.extend.
IIRC
o.extend m
is a shortcut for
class << o; include m end

Cheers
Robert

C’est véritablement utile puisque c’est joli.

Antoine de Saint Exupéry

I’ve modified my version by getting inspired by your code :slight_smile:
In this case there is no stack. You can remove any arbitrary
behaviour/module, which can be an advantage or a risk.

module Behaviourable
def extend mod
@ancestors ||= {}
return if @ancestors[mod]
mod_clone = mod.clone
@ancestors[mod] = mod_clone
super mod_clone
end

def remove mod
mod_clone = @ancestors[mod]
mod_clone.instance_methods.each {|m| mod_clone.module_eval {
remove_method m } }
@ancestors[mod] = nil
end
end

class A
include Behaviourable
end

module NormalBehaviour
def talk; puts ‘Hi’; end
end
module EnhancedBehaviour
def talk; super; puts ‘Goodbye’; end
end

a = A.new
a.extend NormalBehaviour
a.talk # Hi
a.extend EnhancedBehaviour
a.talk # Hi
# Goodbye
a.remove EnhancedBehaviour
a.talk # Hi
a.extend EnhancedBehaviour
a.extend EnhancedBehaviour # Nothing happens
a.talk # Hi
# Goodbye

Robert D. wrote:

On Mon, Sep 1, 2008 at 10:34 PM, Jos� Ignacio
[email protected] wrote:

Thanks, that’s a really nice piece of code that helps a lot :slight_smile:

Why use a @bsp variable instead of using @behavior_stack.pop or
@behavior_stack.last ?
Good question ;). Actually I am leaving empty modules on the stack
that can be reused, thus
after a push_behavior the stack might look like this [ x, module with
the methods from pushed behavior ] and the sp is 1
after pop_behavior the situation is as follows stack : [x, empty
module] and the sp=0.
This avoids unnecessary recreation of modules ( and lets the GC rest,
but that was not the idea :wink:

Nice :slight_smile: That’s not the case in my code, where objects are created and
deleted everytime you add or remove a behaviour.

It seems that the trick to undo Object.extend is done by erasing all
instance methods in the modules. Isn’t this a bit strange? If Ruby
allows including modules in an instance’s ancestor chain, there should
be a way to remove them…
I am not sure to understand this question, do you mean removing them
from the includee?
That would not work, sorry if I miss the obvious here maybe you might
elaborate.

Trouble is that I don’t really know what happens in the background in
Ruby when you use Object.extend.

I suppose an internal module chain is kept in instances in a similar way
as it happens with classes:
A.ancestors # => [A, Behaviourable, Object, Kernel]

a.extend NormalBehaviour # should make something like a.internal_chain =
[NormalBehaviour.clone, A, Behaviourable, Object, Kernel]

If we remove all methods from that NormalBehaviour.clone and re-extend
the object with that module, the chain will grow with an empty module:

a.remove NormalBehaviour # a.internal_chain =
[NormalBehaviour.clone.empty!, A, Behaviourable, Object, Kernel]

a.extend NormalBehaviour # a.internal_chain = [NormalBehaviour.clone,
NormalBehaviour.clone.empty!, A, Behaviourable, Object, Kernel]

But that’s my way of viewing this… I don’t really know what happens in
the background :slight_smile:

Cheers
Jose

IIRC
o.extend m
is a shortcut for
class << o; include m end

I think you’re right, after reviewing what Why points out at
http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html .
This makes me wonder again if there are any performance issues due to
leaving empty modules in the object’s metaclass ancestor chain, but that
might depend on Ruby’s implementation.

Benchmarks come to save us:

require ‘benchmark’
module Mod; def method; 3*50+1; end; end
a = A.new; a.extend Mod
b = A.new; b.extend Mod
Benchmark.bm(30) do |x|
x.report(‘Removing & extending (1)’) { 1000.times {b.remove(Mod);
b.extend(Mod)} }
x.report(‘Removing & extending (2)’) { 1000.times {b.remove(Mod);
b.extend(Mod)} }
x.report(‘Removing & extending (3)’) { 1000.times {b.remove(Mod);
b.extend(Mod)} }
x.report(‘Removing & extending (4)’) { 1000.times {b.remove(Mod);
b.extend(Mod)} }
x.report(‘Removing & extending (5)’) { 1000.times {b.remove(Mod);
b.extend(Mod)} }
x.report(‘Removing & extending (7)’) { 1000.times {b.remove(Mod);
b.extend(Mod)} }
x.report(‘Removing & extending (8)’) { 1000.times {b.remove(Mod);
b.extend(Mod)} }
x.report(‘Removing & extending (9)’) { 1000.times {b.remove(Mod);
b.extend(Mod)} }
x.report(‘Removing & extending (10)’) { 1000.times {b.remove(Mod);
b.extend(Mod)} }
x.report(‘a.method’) { 5000000.times {a.method} }
x.report(‘b.method’) { 5000000.times {b.method} }
end

Output:
user system total
real
Removing & extending (1) 0.040000 0.000000 0.040000 (
0.044988)
Removing & extending (2) 0.090000 0.000000 0.090000 (
0.093711)
Removing & extending (3) 0.130000 0.000000 0.130000 (
0.132898)
Removing & extending (4) 0.170000 0.000000 0.170000 (
0.168743)
Removing & extending (5) 0.210000 0.000000 0.210000 (
0.214160)
Removing & extending (7) 0.260000 0.010000 0.270000 (
0.266014)
Removing & extending (8) 0.330000 0.000000 0.330000 (
0.327045)
Removing & extending (9) 0.400000 0.000000 0.400000 (
0.404618)
Removing & extending (10) 0.520000 0.000000 0.520000 (
0.519781)
a.method 5.290000 1.140000 6.430000 (
6.428015)
b.method 5.080000 1.130000 6.210000 (
6.390391)

It’s suprising, but removing and extending slows down increasingly,
though there is no slow down in the method call (i.e. a & b have the
same performance).

I’ve tried to repeat the tests commenting out “super mod_clone” in
Behaviourable.extend and now there’s no slow down, so the performance
issue is related with Object.extend.