Hi David,
Array#replace seems to be like collect!, it's a nicer way
around the specific problem for Array only. Well, *some*
classes only. Hash, String and Array all seem to have
#replace. But Fixnum, Bignum, Float, and File (to name
the few I just tested right now) do not.
Fixnum could not possibly have it, because fixnum values are
stored directly in the pointers. That is, if you do this,
a = 123
b = a
c = a
then there will be three fixnums, one in each pointer.
This is also true for symbols, true, false, and nil.
I'm asking to see if there's a general way to do it in
any class. [...] How would you implement #replace in an
arbitrary class that didn't already support it, such as
Float or StringIO?
The state of a normal Ruby object depends entirely on its
instance variables. Hence, normal objects can be replaced
quite easily, by simply replacing their instance variables.
class Object
def remove_instance_variables
for ivar in instance_variables do
remove_instance_variable(ivar)
end
end
def merge_instance_variables(other)
for ivar in other.instance_variables do
value = other.instance_variable_get(ivar)
instance_variable_set(ivar, value)
end
end
def replace(other)
copy = other.clone
remove_instance_variables
merge_instance_variables(copy)
return self
end
end
class Foo
attr_accessor :x
def initialize(x) @x = x end
end
a = Foo.new(1) ; b = Foo.new(2)
b #=> #<Foo:0xb7a8a360 @x=2>
b.replace(a)
b #=> #<Foo:0xb7a8a360 @x=1>
a.x = 3
b #=> #<Foo:0xb7a8a360 @x=1>
(Of course, strange things is likely to happen if you try to
use this method to change the type of an object.)
There is one subtlety: neither Array#replace, Hash#replace,
String#replace nor Set#replace cause the two objects to
become shallow aliases of each other:
a = [1] ; b = ; b.replace(a) ; a << 2 ; b #=>
a = {1=>2} ; b = {} ; b.replace(a) ; a[3] = 4 ; b #=> {1=>2}
a = "x" ; b = "" ; b.replace(a) ; a << "y" ; b #=> "x"
require "set"
a = Set[1] ; b = Set ; b.replace(a)
a << 2 ; b #=> #<Set: {1}>
That's why I defined Object#replace to clone the argument
before snarfing its instance variables:
require "ostruct"
p = OpenStruct.new
q = OpenStruct.new
p.foo = 123
q.replace(p)
q #=> <OpenStruct foo=123>
p.bar = 456
q #=> <OpenStruct foo=123>
However, none of the classes you mentioned (Float and
StringIO) are "normal" in the sense required for this
implementation of Object#replace to work:
require "stringio"
p = StringIO.new("foo")
q = StringIO.new("bar")
p.read(1) #=> "f"
p.replace(q)
# Though all p's instance variables have been replaced by
# those of a copy of q, the old state of p remains:
p.read(1) #=> "o"
# This happens because StringIO objects do not use instance
# variables to store their data:
p.instance_variables #=>
q.instance_variables #=>
# The same is true for Float objects:
a = 1.0 ; b = 2.0
a.instance_variables #=>
b.instance_variables #=>
b.replace(a) #=> 2.0
So how would one implement Float#replace? You can't do it
without manually modifying the piece of memory in which the
floating-point value is stored. This happens to be eight
bytes at an offset of eight bytes into the Float object,
assuming that longs and pointers are 32 bit wide.
The standard #replace methods do not copy singleton methods
nor instance variables, so Float#replace should reasonably
not have to do this either. (In fact, floats cannot even
have singleton methods. They *can* have instance variables,
however --- these are stored in a global table keyed by the
object's memory address instead of in the object itself.)
Florian Groß and Julio Fernández Pradier have implemented a
method called Object#become, which uses DL::PtrData to
"simply" overwrite one object with (a copy of) another.
See evil.rb, which can be found at the following URL:
http://rubyforge.org/cgi-bin/viewcvs.cgi/evil/lib/evil.rb?rev=1.40&cvsroot=evil
I hope this helps,
···
--
Daniel Brockman <daniel@brockman.se>