1

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?

2
  • 1
    Don't forget syntax features like @starting_value += 1. That's usually less verbose and avoids typographical errors. Commented Aug 2, 2016 at 15:37
  • 1
    This code is pretty much line-for-line identical to the example in the "Common gotchas" section of the docs for Array::new which answer this very question. Commented Aug 2, 2016 at 15:42

3 Answers 3

5

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.

Sign up to request clarification or add additional context in comments.

6 Comments

thanks! That code was literally the first 21 lines of ruby I've ever written, so I appreciate all improvements.
Hope that works out for you. Seems like you're already off and running, which is good to see.
Very clear answer. Perhaps def initialize(initial=0); @value = initial; end.
@CarySwoveland I try to default to nil as sometimes values of that sort sneak through inadvertently. = 0 gives a sort of false confidence that it will be forced to 0 by default, but it's only 0 if the argument is not specified, a subtle but important distinction.
You don't want an "inadvertent" nil to raise an exception?
|
2

Try

counter_arr = Array.new(5) { Counter.new(0) } 

Comments

1
counter_arr = ([-> { Counter.new(0) }] * 5).map &:call

4 Comments

That's a pretty crazy solution, but it is at least novel.
I think the other answers provide cleaner solutions, but I am still curious as to what is going on here
It is Ursus's answer expressed inside-out.
Well, first of all I upvoted @tadman’s answer, and then I recalled that I’m the “just-out-of-curiosity-clown” here. So: the Counter constructor is wrapped withing a proc to delay it’s execution, than an array of size 1 is created out of this proc, then this array is being spread to have 5 elements using Array#* and finally it’s mapped to actual counters by invoking procs.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.