Mutable member variables -- surprising behavior

I noticed some surprising behavior in one of my Ruby programs, and am
wondering what the rationale is. Basically,
given read-only access to a member variable (using attr_reader) of
class Array, I can modify that Array by modifying (what I think should
be) a local copy of it. Shouldn't the accessor return a copy of the
variable instead of a reference to it? Or is it standard practice to
hand-write an accessor for arrays that returns a clone of the array?

#!/usr/bin/ruby

class Example
  attr_reader :elems

  def initialize
    @elems = []
    3.times { @elems << Object.new }
  end

  def to_s
    @elems.inspect
  end
end

e = Example.new
puts e
# output is:
# -604282308
# [#<Object:0xb7f6c450>, #<Object:0xb7f6c43c>, #<Object:0xb7f6c428>]

list = e.elems
puts list.object_id
# output is: -604282308
list.delete_at(0)

puts e
# output is
# -604282308
# [#<Object:0xb7f6c43c>, #<Object:0xb7f6c428>]

You misunderstand what "attr_reader" does. The "attr_" methods simply
generate setter / getter methods on your class for instance variables.

attr_reader :variable

  def variable; @variable; end

attr_writer :variable

  def variable=(val); @variable = val; end

attr_accessor :variable creates both.

If you want something truely immutable, you have to #freeze it, but
then it's not changeable inside the class either.

If you're really worried about this, you can manually do:

class Example

def initialize
   @elems =
   3.times { @elems << Object.new }
end

def to_s
   @elems.inspect
end

# Always give a copy of the array, that way the internal @elems
# array is never changed.
def elems
   @elems.clone
end
end

Hope that helps.

Jason R.

···

On Mon, Apr 14, 2008 at 1:25 PM, Adam Bender <abender@gmail.com> wrote:

I noticed some surprising behavior in one of my Ruby programs, and am
wondering what the rationale is. Basically,
given read-only access to a member variable (using attr_reader) of
class Array, I can modify that Array by modifying (what I think should
be) a local copy of it. Shouldn't the accessor return a copy of the
variable instead of a reference to it? Or is it standard practice to
hand-write an accessor for arrays that returns a clone of the array?

#!/usr/bin/ruby

class Example
  attr_reader :elems

  def initialize
    @elems =
    3.times { @elems << Object.new }
  end

  def to_s
    @elems.inspect
  end
end

e = Example.new
puts e
# output is:
# -604282308
# [#<Object:0xb7f6c450>, #<Object:0xb7f6c43c>, #<Object:0xb7f6c428>]

list = e.elems
puts list.object_id
# output is: -604282308
list.delete_at(0)

puts e
# output is
# -604282308
# [#<Object:0xb7f6c43c>, #<Object:0xb7f6c428>]

This was recently discussed on this list. See:
http://groups.google.com/group/comp.lang.ruby/browse_thread/thread/18058b9f36bdd1a7/

But the short answer is: Yes, you'll have to write your own accessor
if you don't want this behavior. All attr_reader does is define the
accessor, which just returns the object. You don't get a setter
method (elems=), but that doesn't make the object immutable.

HTH,
Chris

···

On Apr 14, 11:25 am, Adam Bender <aben...@gmail.com> wrote:

I noticed some surprising behavior in one of my Ruby programs, and am
wondering what the rationale is. Basically,
given read-only access to a member variable (using attr_reader) of
class Array, I can modify that Array by modifying (what I think should
be) a local copy of it. Shouldn't the accessor return a copy of the
variable instead of a reference to it? Or is it standard practice to
hand-write an accessor for arrays that returns a clone of the array?

#!/usr/bin/ruby

class Example
  attr_reader :elems

  def initialize
    @elems =
    3.times { @elems << Object.new }
  end

  def to_s
    @elems.inspect
  end
end

e = Example.new
puts e
# output is:
# -604282308
# [#<Object:0xb7f6c450>, #<Object:0xb7f6c43c>, #<Object:0xb7f6c428>]

list = e.elems
puts list.object_id
# output is: -604282308
list.delete_at(0)

puts e
# output is
# -604282308
# [#<Object:0xb7f6c43c>, #<Object:0xb7f6c428>]

does not work, neither does dup. both are rather shallow in ruby. if you *really* want a copy you need

   Marshal.load(Marshal.dump(@elems))

otherwise you'll end up with subtle bugs when the array is cloned, but elements inside of it, or deeper, are not.

fyi.

a @ http://codeforpeople.com/

···

On Apr 14, 2008, at 11:34 AM, Jason Roelofs wrote:

# Always give a copy of the array, that way the internal @elems
# array is never changed.
def elems
  @elems.clone
end

--
we can deny everything, except that we have the possibility of being better. simply reflect on that.
h.h. the 14th dalai lama

Eep, yeah, I forgot about that. #dup and #clone won't work, sorry for
that misinformation.

Jason

···

On Mon, Apr 14, 2008 at 1:43 PM, ara.t.howard <ara.t.howard@gmail.com> wrote:

On Apr 14, 2008, at 11:34 AM, Jason Roelofs wrote:

>
> # Always give a copy of the array, that way the internal @elems
> # array is never changed.
> def elems
> @elems.clone
> end
> end
>

does not work, neither does dup. both are rather shallow in ruby. if you
*really* want a copy you need

  Marshal.load(Marshal.dump(@elems))

otherwise you'll end up with subtle bugs when the array is cloned, but
elements inside of it, or deeper, are not.

fyi.

a @ http://codeforpeople.com/
--
we can deny everything, except that we have the possibility of being
better. simply reflect on that.
h.h. the 14th dalai lama

well they *might* :wink:

i've killed a lot of time over the years on that one ;-(

cheers.

a @ http://codeforpeople.com/

···

On Apr 14, 2008, at 12:02 PM, Jason Roelofs wrote:

Eep, yeah, I forgot about that. #dup and #clone won't work, sorry for
that misinformation.

--
we can deny everything, except that we have the possibility of being better. simply reflect on that.
h.h. the 14th dalai lama