Kissenger
Kissenger

Reputation: 395

Ruby variable behaviour when calling a method

I'm pretty good at getting answers from google, but I just don't get this. In the following code, why does variable 'b' get changed after calling 'addup'? I think I understand why 'a' gets changed (although its a bit fuzzy), but I want to save the original array 'a' into 'b', run the method on 'a' so I have two arrays with different content. What am I doing wrong?

Thanks in advance

def addup(arr)

  i=0
  while i< arr.length
    if arr[i]>3
      arr.delete_at(i)
    end
    i += 1
  end

  return arr

end

a = [1,2,3,4]
b = a

puts "a=#{a}"             # => [1,2,3,4]
puts "b=#{b}"             # => [1,2,3,4]
puts "addup=#{addup(a)}"  # => [1,2,3]
puts "a=#{a}"             # => [1,2,3]
puts "b=#{b}"             # => [1,2,3]

Upvotes: 1

Views: 69

Answers (3)

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369458

In the following code, why does variable 'b' get changed after calling 'addup'?

The variable doesn't get changed. It still references the exact same array it did before.

There are only two ways to change a variable in Ruby:

  1. Assignment (foo = :bar)
  2. Reflection (Binding#local_variable_set, Object#instance_variable_set, Module#class_variable_set, Module#const_set)

Neither of those is used here.

I think I understand why 'a' gets changed (although its a bit fuzzy)

a doesn't get changed either. It also still references the exact same array it did before. (Which, incidentally, is the same array that b references.)

The only thing which does change is the internal state of the array that is referenced by both a and b. So, if you really understand why the array referenced by a changes, then you also understand why the array referenced by b changes, since it is the same array. There is only one array in your code.

The immediate problem with your code is that, if you want a copy of the array, then you need to actually make a copy of the array. That's what Object#dup and Object#clone are for:

b = a.clone

Will fix your code.

BUT!

There are some other problems in your code. The main problem is mutation. If at all possible, you should avoid mutation (and side-effects in general, of which mutation is only one example) as much as possible and only use it when you really, REALLY have to. In particular, you should never mutate objects you don't own, and this means you should never mutate objects that were passed to you as arguments.

However, in your addup method, you mutate the array that is passed to you as arr. Mutation is the source of your problem, if you didn't mutate arr but instead returned a new array with the modifications you want, then you wouldn't have the problem in the first place. One way of not mutating the argument would be to move the cloneing into the method, but there is an even better way.

Another problem with your code is that you are using a loop. In Ruby, there is almost never a situation where a loop is the best solution. In fact, I would go so far as to argue that if you are using a loop, you are doing it wrong.

Loops are error-prone, hard to understand, hard to get right, and they depend on side-effects. A loop cannot work without side-effects, yet, we just said we want to avoid side-effects!

Case in point: your loop contains a serious bug. If I pass [1, 2, 3, 4, 5], the result will be [1, 2, 3, 5]. Why? Because of mutation and manual looping:

In the fourth iteration of the loop, at the beginning, the array looks like this:

[1, 2, 3, 4, 5]
#         ↑
#         i

After the call to delete_at(i), the array looks like this:

[1, 2, 3, 5]
#         ↑
#         i

Now, you increment i, so the situation looks like this:

[1, 2, 3, 5]
#            ↑
#            i

i is now greater than the length of the array, ergo, the loop ends, and the 5 never gets removed.

What you really want, is this:

def addup(arr)
  arr.reject {|el| el > 3 }
end

a = [1, 2, 3, 4, 5]
b = a

puts "a=#{a}"             # => [1, 2, 3, 4, 5]
puts "b=#{b}"             # => [1, 2, 3, 4, 5]
puts "addup=#{addup(a)}"  # => [1, 2, 3]
puts "a=#{a}"             # => [1, 2, 3, 4, 5]
puts "b=#{b}"             # => [1, 2, 3, 4, 5]

As you can see, nothing was mutated. addup simply returns the new array with the modifications you want. If you want to refer to that array later, you can assign it to a variable:

c = addup(a)

There is no need to manually fiddle with loop indices. There is no need to copy or clone anything. There is no "spooky action at a distance", as Albert Einstein called it. We fixed two bugs and removed 7 lines of code, simply by

  • avoiding mutation
  • avoiding loops

Upvotes: 2

Sergio Tulentsev
Sergio Tulentsev

Reputation: 230336

but I want to save the original array 'a' into 'b'

You are not saving the original array into b. Value of a is a reference to an array. You are copying a reference, which still points to the same array. No matter which reference you use to mutate the array, the changes will be visible through both references, because, again, they point to the same array.

To get a copy of the array, you have to explicitly do that. For shallow arrays with primitive values, simple a.dup will suffice. For structures which are nested or contain references to complex objects, you likely need a deep copy. Something like this:

b = Marhal.load(Marshal.dump(a))

Upvotes: 3

toniedzwiedz
toniedzwiedz

Reputation: 18553

Both a and b hold a reference to the same array object in memory. In order to save the original array in b, you'd need to copy the array.

a = [1,2,3,4] # => [1, 2, 3, 4]
b = a         # => [1, 2, 3, 4]
c = a.dup     # => [1, 2, 3, 4]
a.push 5      # => [1, 2, 3, 4, 5]
a             # => [1, 2, 3, 4, 5]
b             # => [1, 2, 3, 4, 5]
c             # => [1, 2, 3, 4]

For more information on why this is happening, read Is Ruby pass by reference or by value?

Upvotes: 4

Related Questions