Enumerator `Array#each` 's {block} can't always change array values?
Asked Answered
A

4

6

Ok maybe this is simple but... given this:

arr = ("a".."z").to_a

arr

=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

..and that I'm trying to change all "arr" values to "bad"

why isn't this working ?

arr.each { |v| v = "bad" }

arr

=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

Answers suggested that "v" is a local variable to the block (a "copy" of the array value) and I fully understand that (and never puzzled me before) but then

.. why it is working if array elements are objects ?

class Person
  def initialize
    @age = 0
  end
  attr_accessor :age
end

kid = Person.new
man = Person.new
arr = [kid, man]


arr.each { |p| p.age = 50 }

arr[0]
=> #<Person:0xf98298 @age=50>

isn't here "p" still local to the block here? but then it really affects the objects, how come ?

Aliciaalick answered 23/10, 2012 at 1:15 Comment(2)
v is a local variable that is bound to the value that was in the index. It is not a "reference" to the original array slot!Anisole
Objects are values too! - Ruby uses Call By Object (Sharing) which explains the mutation side-effects - it's the same value/object! The key importance is a variable is not a value.Anisole
D
11

I'll expand upon @pst's comment:

why isn't this working ?

arr.each { |v| v = "bad" }

Because each iterates through the array and puts each item into the block you've given as a local variable v, as v is not a reference to the array arr.

new_arr = arr.each { |v| v = "bad" }

each does not give back an array, for that you would use map (see @benjaminbenben's answer). Therefore assigning it does not "work".

arr.each { |v| arr[arr.index v] = "bad" }

Here you put each item in arr into the local variable v, but you've also referred to the array itself in the block, hence you are able to assign to the array and use the local variable v to find an index that corresponds to the contents of v (but you may find this wouldn't work as you expect when the items are not all unique).

arr.each { |p| p.age = 50 }

kid.age #-> 50

Here, again you've filled the local variable p with each item/object in arr, but then you've accessed each item via a method, so you are able to change that item - you are not changing the array. It's different because the reference is to the contents of the local variable, which you've mixed up with being a reference to the array. They are separate things.


In response to the comment below:

arr[0]
# => #<Person:0xf98298 @age=50>

It's all about who's referring to whom when.

Try this:

v = Person.new
# => #<Person:0x000001008de248 @age=0>
w = Person.new
# => #<Person:0x000001008d8050 @age=0>
x = v
# => #<Person:0x000001008de248 @age=0>
v = Person.new
# => #<Person:0x00000100877e80 @age=0>
arr = [v,w,x]
# => [#<Person:0x00000100877e80 @age=0>, #<Person:0x000001008d8050 @age=0>, #<Person:0x000001008de248 @age=0>]

v referred to 2 different objects there. v is not a fixed thing, it's a name. At first it refers to #<Person:0x000001008de248 @age=0>, then it refers to #<Person:0x00000100877e80 @age=0>.

Now try this:

arr.each { |v| v = "bad" }
# => [#<Person:0x00000100877e80 @age=0>, #<Person:0x000001008d8050 @age=0>, #<Person:0x000001008de248 @age=0>]

They are all objects but nothing was updated or "worked". Why? Because when the block is first entered, v refers to the item in the array that was yielded (given). So on first iteration v is #<Person:0x00000100877e80 @age=0>.

But, we then assign "bad" to v. We are not assigning "bad" to the first index of the array because we aren't referencing the array at all. arr is the reference to the array. Put arr inside the block and you can alter it:

arr.each { |v| 
  arr[0] = "bad" # yes, a bad idea!
}

Why then does arr.each { |p| p.age = 50 } update the items in the array? Because p refers to the objects that also happen to be in the array. On first iteration p refers to the object also known as kid, and kid has an age= method and you stick 50 in it. kid is also the first item in the array, but you're talking about kid not the array. You could do this:

arr.each { |p| p = "bad"; p.age }
NoMethodError: undefined method `age' for "bad":String

At first, p referred to the object that also happened to be in the array (that's where it was yielded from), but then p was made to refer to "bad".

each iterates over the array and yields a value on each iteration. You only get the value not the array. If you want to update an array you either do:

new_arr = arr.map{|v| v = "bad" }
new_arr = arr.map{|v| "bad" } # same thing

or

arr.map!{|v| v = "bad"}
arr.map!{|v| "bad"}  # same thing

as map returns an array filled with the return value of the block. map! will update the reference you called it on with an array filled with the return value of the block. Generally, it's a bad idea to update an object when iterating over it anyway. I find it's always better to think of it as creating a new array, and then you can use the ! methods as a shortcut.

Discommode answered 23/10, 2012 at 1:50 Comment(3)
i had mistyped my "test query": kid.age #=> 50 It was instead arr[0] # => #<Person:0xf98298 @age=50> . So doesn't this show that the actual array contains a changed value ?Aliciaalick
@lain i edited the question with the changes so you can read it better if you wish, thanksAliciaalick
@lain thanks now it's all clear, thanks for the time you've spent on this! :) I really needed a thorough explaination! Voting best answer here, and hoping other people would benefit from your answer! Well done man!Aliciaalick
H
4

In example

arr.each { |v| v = "bad" }

"v" is just reference to string, when you do v = "bad", you reassign local variable. To make everything bad you can do like that:

arr.each { |v| v.replace "bad" }

Next time you can play with Object#object_id

puts arr[0].object_id #will be save as object_id in first iteration bellow
arr.each { |v| puts v.object_id }
Higley answered 23/10, 2012 at 3:1 Comment(1)
nice, i didn't know .replace method :) +1Aliciaalick
I
3

You might be looking for .map - which returns a new array with the the return value of the block for each element.

arr.map { "bad" }

=> ["bad", "bad", "bad", "bad", …] 

using .map! will alter the contents of the original array rather than return a new one.

Inclusive answered 23/10, 2012 at 1:28 Comment(2)
thanks I will probably follow this way, but still I am questioning myself why my first solution does work in a case and does not in another (check my edit) thanksAliciaalick
ah! @Iain's answer seems to cover that pretty well!Inclusive
C
1

How about this

arry = Array.new(arry.length,"bad")

This will set the a default value of "bad" to the arry.length

Clyve answered 23/10, 2012 at 10:34 Comment(2)
what the..! I can believe I missed this! It's the easiest, more convenient and elegant way to solve the problem I was trying to solve in the first time ! Thanks! Next time i'll spend more time reading on core classes .new method! Upvoting!Aliciaalick
p.s.: if you use parentesys for Array.new(parameter) remember they must come after .new without spaces! -> Class.new(parameter). If you have a space between .new and (parameter) you will get syntax error. Just thought it could be useful to remind it (to myself for first) :Aliciaalick

© 2022 - 2024 — McMap. All rights reserved.