Dustin Ryan-Roepsch Dustin Ryan-Roepsch - 4 months ago 12
Ruby Question

Filling a ruby array with a default object

Let's say I have a class Counter in ruby, defined as

class Counter
attr_accessor :starting_value
def initialize(starting_value)
@starting_value = starting_value
end

def tick
@starting_value = @starting_value + 1
end
end


and I want to fill an array with that object, using a default parameter, like this:
counter_arr = Array.new(5, Counter.new(0))


This is almost what I want, except that I now have an array that contains the same instance of a counter 5 times, instead of an array of 5 new counters. IE when I run the code

counter_arr = Array.new(5, Counter.new(0))
counter_arr[0].tick
counter_arr.each do |c|
puts(c.starting_value)
end


I output

1
1
1
1
1


instead of

1
0
0
0
0


I was wondering, what is the "ruby-esque" way to initialize an array with multiple new instances of an object?

Answer

One of the first major stumbling blocks people encounter when learning Ruby if they're unfamiliar with a language that uses object reverences pervasively is how these work.

An array is a collection of references to zero or more other objects. These objects are not necessarily unique, and in some cases they are all identical. You are creating such an object here:

counters = Array.new(5, Counter.new(0))

This creates a singular Counter object and populates all 5 slots of the array with it. This comes about because arguments to methods are evaluated once before the method is called. You can test this:

counters.map(&:object_id)

That returns the unique object ID for each object in the array. They'll be random values, each process is different, but they will be identical.

The way to fix this is to use the block initializer:

counters = Array.new(5) do
  Counter.new(0)
end

That doesn't insert the same object, but the result of evaluating that block each time, and since that initializes a new Counter object, the objects will be unique.

One way to tidy this up is to adjust your Counter object to have a sane default:

class Counter
  def initialize(initial = nil)
    @value = initial.to_i
  end

  def tick
    @value += 0
  end
end

This has the advantage of accepting arbitrary values, even those that aren't necessarily the right type. Now Counter.new('2') will work with that value being converted automatically. This is the fundamental principle of Duck Typing. If it can give you a number, it's as good as a number.

Comments