Reopening with a bang: cannot assign to self

(David Brady) #1

Hello,

I'm writing some code that benefits from extending Array. One of the methods wants to be a bang method. How do I reassign the contents of an Array from within it? (I recognize that subclassing Array might be more proper, but I think the problem exists in the subclass as well.)

class Array
  # returns copy of array, with each value divided by the sum of
  # the array elements. e.g. [1,2,2] => [0.2, 0.4, 0.4].
  # (this code works fine)
  def normalize
    total = Float( self.inject {|x,y| x+y} )
    self.collect {|x| x/total}
  end

  # returns modified array unless array.sum == 1.0, in which case
  # array is not modified and method returns nil
  # (this code does not work: syntax error - cannot assign to self)
  def normalize!
    total = Float( self.inject {|x,y| x+y} )
    if total.zero?
      nil
    else
      self = self.collect {|x| x/total}
    end
  end
end

I can work around the problem by iterating and using self.[]= but the collect is so much cleaner, I have to wonder if there's not a way to use it.

Hrm, I've just found "Array#collect!", which will do the trick I need, but I still wonder what the proper Ruby idiom is for extending a class with a bang method that essentially modifies or replaces the entire contents of the object, when there isn't an accessor method. For example, how would you do "Fixnum#div!(x)", e.g. the equivalend of f /= x?

I recognize that this entire line of reasoning may be the result of wrongheaded thinking about Ruby. If so, please enlighten me.

-dB

···

--
David Brady
ruby_talk@shinybit.com
I'm feeling really surreal today... OR AM I?

(Daniel Amelang) #2

Is Array#replace what you're looking for?

Dan

···

On 8/27/05, David Brady <ruby_talk@shinybit.com> wrote:

Hello,

I'm writing some code that benefits from extending Array. One of the
methods wants to be a bang method. How do I reassign the contents of an
Array from within it? (I recognize that subclassing Array might be more
proper, but I think the problem exists in the subclass as well.)

class Array
  # returns copy of array, with each value divided by the sum of
  # the array elements. e.g. [1,2,2] => [0.2, 0.4, 0.4].
  # (this code works fine)
  def normalize
    total = Float( self.inject {|x,y| x+y} )
    self.collect {|x| x/total}
  end

  # returns modified array unless array.sum == 1.0, in which case
  # array is not modified and method returns nil
  # (this code does not work: syntax error - cannot assign to self)
  def normalize!
    total = Float( self.inject {|x,y| x+y} )
    if total.zero?
      nil
    else
      self = self.collect {|x| x/total}
    end
  end
end

I can work around the problem by iterating and using self.[]= but the
collect is so much cleaner, I have to wonder if there's not a way to use it.

Hrm, I've just found "Array#collect!", which will do the trick I need,
but I still wonder what the proper Ruby idiom is for extending a class
with a bang method that essentially modifies or replaces the entire
contents of the object, when there isn't an accessor method. For
example, how would you do "Fixnum#div!(x)", e.g. the equivalend of f /= x?

I recognize that this entire line of reasoning may be the result of
wrongheaded thinking about Ruby. If so, please enlighten me.

-dB

--
David Brady
ruby_talk@shinybit.com
I'm feeling really surreal today... OR AM I?

(David Brady) #3

Daniel Amelang wrote:

Is Array#replace what you're looking for?

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

I'm asking to see if there's a general way to do it in any class.

Actually, replace is an even better example of my question. How would you implement #replace in an arbitrary class that didn't already support it, such as Float or StringIO?

-dB

···

--
David Brady
ruby-talk@shinybit.com
I'm having a really surreal day... OR AM I?

(Daniel Amelang) #4

I see. Well, there isn't a general way to do it in any class, since
some classes (like Fixnum) are immutable.

For most classes, though, there will be a way to do what you want by
finding a 'replace'-like method.

Dan

(Daniel Brockman) #5

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>