Extending an Instance only Once

G

Gavin Kistner

--Apple-Mail-1-556835652
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
delsp=yes;
format=flowed

I have a method that requires that the arguments passed to it have
been in-mixed with a module. Currently, my code looks like this:

def my_method( the_obj )
the_obj.extend( MyModule ) unless the_obj.inherits_from?
( MyModule )
end

class Object
def selfclass
class << self; self; end
end

# Return true if the instance inherits from the supplied
_class_or_module_.
def inherits_from?( class_or_module )
self.class.ancestors.include?( class_or_module ) or
self.selfclass.ancestors.include?( class_or_module )
end
end


Is there a better way to do that? (I'm testing if the object inherits
from a class which was extended by the module, OR if the instance
itself was already extended.) Does it end up being cheaper/faster to
just always re-extend the instance? (What happens if you extend both
the class and an instance of that class? Does the singleton class
take precedence?)

--
"When I am working on a problem I never think about beauty. I only
think about how to solve the problem. But when I have finished, if
the solution is not beautiful, I know it is wrong."
- R. Buckminster Fuller


--Apple-Mail-1-556835652--
 
D

David A. Black

HI --

I have a method that requires that the arguments passed to it have been
in-mixed with a module. Currently, my code looks like this:

def my_method( the_obj )
the_obj.extend( MyModule ) unless the_obj.inherits_from?( MyModule )
end

class Object
def selfclass
class << self; self; end
end

# Return true if the instance inherits from the supplied
_class_or_module_.
def inherits_from?( class_or_module )
self.class.ancestors.include?( class_or_module ) or
self.selfclass.ancestors.include?( class_or_module )
end
end


Is there a better way to do that?

Unless I'm missing something, #is_a? would do it.
(I'm testing if the object inherits from a
class which was extended by the module, OR if the instance itself was already
extended.) Does it end up being cheaper/faster to just always re-extend the
instance? (What happens if you extend both the class and an instance of that
class? Does the singleton class take precedence?)

The singleton class takes precedence in the sense that it comes
earlier in the method lookup path. (I'm assuming that you mean mixing
the module into the class, not MyClass.extend(MyMod), which would not
affect the class's instances.)

Overall, I'm tempted to say that if you're extending a single object
multiple times with the same module, there's something not quite right
in the program flow. But I doubt it's going to slow things down very
much (but I haven't benchmarked it).


David
 
G

Gavin Kistner

--Apple-Mail-1-587503984
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
delsp=yes;
format=flowed

Unless I'm missing something, #is_a? would do it.

Nope, apparently it was I who missed something. Thanks :)

The singleton class takes precedence in the sense that it comes
earlier in the method lookup path. (I'm assuming that you mean mixing
the module into the class, not MyClass.extend(MyMod), which would not
affect the class's instances.)

Indeed, that is what I meant.
module M; end
class Foo; end
class Bar; include M; end
f = Foo.new
b = Bar.new
f.is_a?( M ) #=> true
b.is_a?( M ) #=> true

Yay :)


Overall, I'm tempted to say that if you're extending a single object
multiple times with the same module, there's something not quite right
in the program flow. But I doubt it's going to slow things down very
much (but I haven't benchmarked it).

Yeah, I could certainly realize _some_ performance gain by ensuring
that the consumer of the class ensures that they pre-mix any
instances which are handed to the method. But I'm going for uber-
friendliness, sort of auto-duck-typing. ("You say you don't quack
like a duck? Here, let me strap this bill onto your head.")

When I get to benchmarking I'll check it out, but I think it should
be fine, since this method is run only a few times during setup, and
not during the cpu-intensive phase.

--Apple-Mail-1-587503984--
 
P

Peter

I have a method that requires that the arguments passed to it have been
in-mixed with a module. Currently, my code looks like this:

def my_method( the_obj )
the_obj.extend( MyModule ) unless the_obj.inherits_from?( MyModule )
end

Ruby already checks this:

irb(main):001:0> module M ; end
=> nil
irb(main):002:0> class A ; end
=> nil
irb(main):003:0> class B < A; end
=> nil
irb(main):004:0> class A ; include M ; end
=> A
irb(main):005:0> class B ; include M ; end
=> B
irb(main):006:0> B.ancestors
=> [B, A, M, Object, Kernel]

Note that there is a single M in there. If the includes happen in the
reverse order, Ruby's check fails and there are two M's in the list:

irb(main):001:0> module M ; end
=> nil
irb(main):002:0> class A ; end
=> nil
irb(main):003:0> class B < A; end
=> nil
irb(main):004:0> class B ; include M ; end
=> B
irb(main):005:0> class A ; include M ; end
=> A
irb(main):006:0> B.ancestors
=> [B, M, A, M, Object, Kernel]

This does not proof the check is actually done, but in the source you can
see that check is actually there without doubt.

The same goes for Object#extend because it is implemented as an include in
the singleton class (or selfclass as some people prefer :) which obeys the
same rules as includes in regular classes. So no need to check this
yourself!

Peter
 
G

Gavin Kistner

--Apple-Mail-4-591371425
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
delsp=yes;
format=flowed

This does not proof the check is actually done, but in the source
you can see that check is actually there without doubt.

The same goes for Object#extend because it is implemented as an
include in the singleton class (or selfclass as some people
prefer :) which obeys the same rules as includes in regular
classes. So no need to check this yourself!

Hrm...I see code that looks like that's what's going on inside of
class.c, but apparently it is faster to test myself:


require 'benchmark'
include Benchmark

module M; end
class Bar < Array; end
class Foo < Bar; end

n = 500000
bm(15){ |x|
f = Foo.new
x.report( "Always Extend" ){
n.times{
x.extend( M )
}
}

f = Foo.new
x.report( "Test First" ){
n.times{
x.extend( M ) unless x.is_a?( M )
}
}
}

user system total real
Always Extend 1.610000 0.010000 1.620000 ( 1.983911)
Test First 0.730000 0.000000 0.730000 ( 0.745857)


Is there a flaw with my test above?

--Apple-Mail-4-591371425--
 
P

peter.vanbroekhoven

Hrm...I see code that looks like that's what's going on inside of
class.c, but apparently it is faster to test myself:


require 'benchmark'
include Benchmark

module M; end
class Bar < Array; end
class Foo < Bar; end

n = 500000
bm(15){ |x|
f = Foo.new
x.report( "Always Extend" ){
n.times{
x.extend( M ) ^^^
}
}

f = Foo.new
x.report( "Test First" ){
n.times{
x.extend( M ) unless x.is_a?( M ) ^^^ ^^^
}
}
}

user system total real
Always Extend 1.610000 0.010000 1.620000 ( 1.983911)
Test First 0.730000 0.000000 0.730000 ( 0.745857)


Is there a flaw with my test above?

You made a typo in your tests (x should be replaced with f), but it
does not influence the result. The only explanation I have is that the
test used in include is more complicated than the one used in #is_a?
because include doesn't only check the module passed as parameter to
#include, it checks modules included in that module too. I guess even
when there are no such modules, there is still some overhead (it's not
exactly a straightforward algorithm). But unless you really intend to
do 500000 extends in a row, I don't think that is really a problem...
:)

Peter
 
C

Christoph

You made a typo in your tests (x should be replaced with f), but it
does not influence the result. The only explanation I have is that the
test used in include is more complicated than the one used in #is_a?
because include doesn't only check the module passed as parameter to
#include, it checks modules included in that module too. I guess even
when there are no such modules, there is still some overhead (it's not
exactly a straightforward algorithm). But unless you really intend to
do 500000 extends in a row, I don't think that is really a problem...
:)
The explanation is probably simpler - f.extend(M) makes hook calls to
M.extend_object(f) and M.extended(f) - M.extend_object(f) probably
tests if f.is_a?(M) a - this means its 4 method calls, versus 1 call +
1 if statement ...
you'll probably find a much smaller difference calling
M.extend_object(f) only.


---
require 'benchmark'
include Benchmark

module M
class Bar < Array; end
class Foo < Bar; end

n = 500000
bm(15){ |x|
f = Foo.new
x.report( "Test First" ){
n.times{
extend_object( f ) unless f.is_a?( M )
}
}
f = Foo.new
x.report( "Always Extend" ){
n.times{
extend_object( f )
}
}
}
end
 
P

peter.vanbroekhoven

Christoph said:
The explanation is probably simpler - f.extend(M) makes hook calls to
M.extend_object(f) and M.extended(f) - M.extend_object(f) probably
tests if f.is_a?(M) a - this means its 4 method calls, versus 1 call +
1 if statement ...
you'll probably find a much smaller difference calling
M.extend_object(f) only.

You're right, of course. I would have noticed that had I actually taken
a look at the code instead of stating things from memory.

M.extend_object(f) doesn't call f.is_a?(M) though; module inclusion
uses its own specialized check. What surprised me a bit though is that
M.extended(f) is called each time, even when f is already extended with
M and is hence not extended again. We could save an extra method call
there (unless people rely on its being called every time Object#extend
is called) and get it down to 2 calls vs 1 call + 1 if statement.

Peter
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
474,172
Messages
2,570,934
Members
47,474
Latest member
AntoniaDea

Latest Threads

Top