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?

···

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

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

···

On Mon, Sep 1, 2008 at 3:39 PM, José Ignacio <joseignacio.fernandez@gmail.com> wrote:

--
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...

···

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

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

···

On Mon, Sep 1, 2008 at 10:34 PM, José Ignacio <joseignacio.fernandez@gmail.com> wrote:

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

--
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 Dober 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

···

On Mon, Sep 1, 2008 at 10:34 PM, Jos� Ignacio > <joseignacio.fernandez@gmail.com> wrote:

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

<snip>

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.
<snip>

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

···

On Tue, Sep 2, 2008 at 12:54 PM, José Ignacio <joseignacio.fernandez@gmail.com> wrote:
--
C'est véritablement utile puisque c'est joli.

Antoine de Saint Exupéry

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.

···

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

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.

···

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