J
Jason Merrill
It seems well known that the Array constructor form
Array.new(n, obj)
causes a common gotcha for programmers new to ruby (see * below for
details). My question: is there ever any reason to use this form in
favor of
Array.new(n) {block}
One time I can imagine someone using the first form is if they wanted
to fill an array with some default value, such as 0, but generally,
default or null values are singletons, or have immediate value. There
is only one nil, and only one 0, and these tend to be immutable. I
can't do anything to change an instance of 0 or nil that would have
surprising side effects. This also means that I could just as easily
write
Array.new(n) {0}
-or-
Array.new(n) { NullObject.instance }
The only way I can see preferring the first method is if you want to
be able to do something to one element of an array that affects the
other elements of the array. Is there ever a good reason to do this?
If, for some reason I can't imagine, you wanted to get the gotcha
behavior out of the second form--maybe you want an array to initially
be filled with the string 'a', but you want the flexibility to easily
decide later that the default really should have been 'b'--you can get
this behavior from the second form without too much difficulty using a
local variable. Concretely, I might use the first form to do
arr = Array.new(6, 'a')
arr[0].gsub!(/a/,'b')
so my array went from being filled with 6 references to the same
instance of 'a' to being filled with 6 references to the same instance
of 'b'. I could duplicate this strange behavior using the second form
by writing
default = 'a'
arr = Array.new(6) {default}
arr[0].gsub!(/a/,'b')
Now that's a little bit harder than with the first form, but that's
the point. It seems like a weird and dangerous thing to do, so it
should be hard, and you should have to be explicit. In light of the
above, maybe Array.new(n, obj) should be deprecated, since it is both
confusing, and can be replicated using the block form with only slight
gymnastics.
When I first started experimenting with ruby about one year ago, the
first thing I did was try to port a version of Conway's Game of Life
into ruby, which I had written in Java for a comp. sci. class. I
wanted a grid of Cells, and tried to implement it as an array of rows,
each containing an array of Cells. This gotcha got me--in a big way.
"Ruby looks so simple on paper," I thought, "but if I get tripped up
trying to write the game of life--what's the point." I didn't bother
reading about ruby any more for a few months. Then I needed to do
some heavy regex lifting, and thankfully decided to give Ruby another
try. I've since fallen in love with the language, and figured out
what I was doing wrong, but I might not have been so lucky.
Cheers,
Jason Merrill
*Common Array Gotcha:
If I want to make an array of 5 hashes, or of containers in general,
if I only skimmed the documentation, I might try to do
menus = Array.new(6, Hash.new)
Of course, the array is filled with a single instance of Hash, so if I do
menus[0]['dessert'] = 'cake'
menus[1]['dessert'] = 'ice cream'
puts menus[0]['dessert']
=> 'ice cream'
This is because menus[0] and menus[1] actually refer to the same
object, and it's being overwritten in the second statement.
What I meant to do was
menus = Array.new(6) {Hash.new}, which executes the block once for
each slot in the array, thus producing 6 new instances of hash.
Array.new(n, obj)
causes a common gotcha for programmers new to ruby (see * below for
details). My question: is there ever any reason to use this form in
favor of
Array.new(n) {block}
One time I can imagine someone using the first form is if they wanted
to fill an array with some default value, such as 0, but generally,
default or null values are singletons, or have immediate value. There
is only one nil, and only one 0, and these tend to be immutable. I
can't do anything to change an instance of 0 or nil that would have
surprising side effects. This also means that I could just as easily
write
Array.new(n) {0}
-or-
Array.new(n) { NullObject.instance }
The only way I can see preferring the first method is if you want to
be able to do something to one element of an array that affects the
other elements of the array. Is there ever a good reason to do this?
If, for some reason I can't imagine, you wanted to get the gotcha
behavior out of the second form--maybe you want an array to initially
be filled with the string 'a', but you want the flexibility to easily
decide later that the default really should have been 'b'--you can get
this behavior from the second form without too much difficulty using a
local variable. Concretely, I might use the first form to do
arr = Array.new(6, 'a')
arr[0].gsub!(/a/,'b')
so my array went from being filled with 6 references to the same
instance of 'a' to being filled with 6 references to the same instance
of 'b'. I could duplicate this strange behavior using the second form
by writing
default = 'a'
arr = Array.new(6) {default}
arr[0].gsub!(/a/,'b')
Now that's a little bit harder than with the first form, but that's
the point. It seems like a weird and dangerous thing to do, so it
should be hard, and you should have to be explicit. In light of the
above, maybe Array.new(n, obj) should be deprecated, since it is both
confusing, and can be replicated using the block form with only slight
gymnastics.
When I first started experimenting with ruby about one year ago, the
first thing I did was try to port a version of Conway's Game of Life
into ruby, which I had written in Java for a comp. sci. class. I
wanted a grid of Cells, and tried to implement it as an array of rows,
each containing an array of Cells. This gotcha got me--in a big way.
"Ruby looks so simple on paper," I thought, "but if I get tripped up
trying to write the game of life--what's the point." I didn't bother
reading about ruby any more for a few months. Then I needed to do
some heavy regex lifting, and thankfully decided to give Ruby another
try. I've since fallen in love with the language, and figured out
what I was doing wrong, but I might not have been so lucky.
Cheers,
Jason Merrill
*Common Array Gotcha:
If I want to make an array of 5 hashes, or of containers in general,
if I only skimmed the documentation, I might try to do
menus = Array.new(6, Hash.new)
Of course, the array is filled with a single instance of Hash, so if I do
menus[0]['dessert'] = 'cake'
menus[1]['dessert'] = 'ice cream'
puts menus[0]['dessert']
=> 'ice cream'
This is because menus[0] and menus[1] actually refer to the same
object, and it's being overwritten in the second statement.
What I meant to do was
menus = Array.new(6) {Hash.new}, which executes the block once for
each slot in the array, thus producing 6 new instances of hash.