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

arrays, block, each, enumerator, ruby

Solution

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.

Problem

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 ?

Original source