2

I just started studying Ruby a little while ago and I was having difficulties with global versus local variable scoping.

Working on a practice problem, I found that an array defined globally was being changed by a function called on it. If I explicitly assign the array to something else, nothing changes. But if I run through and delete items one by one, this deletes them from the global array itself.

Why do delete and pop (which I also tested) methods have this behavior? I understood from reading that this should not be happening, that the "array" inside the functions is a reference to the values of arr, rather than the variable arr.

(I'm using Ruby version 2+)

def change_int x
    x += 2
end

def change_arr array
    array = [4, 5, 6]
end

def pop_arr array
    puts array
    new_array = []

    while array.length > 0
        new_array.push array[0]
        array.delete_at 0
    end

    array
end

x = 5
change_int x
puts x == 5 # true

arr = [1, 2, 3]
change_arr arr
puts arr == [1, 2, 3] # true

old_arr = arr
puts pop_arr arr
puts arr == [1, 2, 3] # false
puts "arr = #{arr}" # arr = []
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
kevlarr
  • 1,070
  • 12
  • 24

2 Answers2

3

You can see by printing #object_id before calling pop_arr and inside pop_arr that those arrays are the same objects. This means that arguments are passed into the function by reference in Ruby.

Here is code:

def pop_arr(array)
  puts array.object_id
  # Rest of the fucntion
end

arr = [1, 2, 3]
puts arr.object_id
pop_arr(arr)

All of this means that when you edit array inside the function it will have effect on the object which was passed. #delete, #delete_at, #pop are operations that change the Array on which they are made.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Bartosz Łęcki
  • 332
  • 2
  • 6
  • When you say `x +=2`, you are saying take the reference x, add 2 to it and reassign it. That means that the thing the reference `x` outside the method is pointing at is not changed. In contrast, with the array, you aren't changing the reference, you are changing the value. – BaroqueBobcat Apr 04 '15 at 00:16
  • No, arguments are not passed by reference in Ruby, they are *always* passed by value, just like in Smalltalk, Java, C, JavaScript, Python, C# (by default), C++ (by default), etc. And unlike C# or C++, there is simply no way to pass by reference at all in Ruby. Here is a simple test to see whether Ruby is pass-by-value or pass-by-reference. Don't take my word for it, just ask Ruby herself! `def foo(bar) bar = 'reference' end; baz = 'value'; foo(baz); puts "Ruby is pass-by-#{baz}" # Ruby is pass-by-value`. – Jörg W Mittag Apr 04 '15 at 12:16
  • See also [Is Ruby pass by reference or by value?](http://stackoverflow.com/a/10330589/2988). If you know how to read C#, I wrote a small example demonstrating the difference between pass-by-value and pass-by-reference. C# is a great language for that, because it has both value types and reference types and supports both pass-by-value and pass-by-reference, so you can demonstrate all four combinations (pass value type by value, pass reference type by value, pass value type be reference, and pass reference type by reference) within a single program: http://stackoverflow.com/a/12438174/2988 – Jörg W Mittag Apr 04 '15 at 12:23
  • Thanks guys, I like the idea of using .object_id to get a better sense of what I'm working on while I'm learning this, and that thread link was really helpful - I haven't read enough about the subtleties of argument passing. – kevlarr Apr 04 '15 at 13:41
  • So, I'm passing the _value_ to the function, but that value is a _pointer_ to an object, and the array outside the function changes because it's pointing to the same object that the array inside the function was pointing to? – kevlarr Apr 04 '15 at 13:50
1

See also: Ruby - Parameters by reference or by value? and Is Ruby pass by reference or by value?.

The curious thing is that change_arr doesn't affect the global array, but pop_arr does, in your code.

Here's what's happening: ruby passes references to objects as parameters. So like Bartosz said, you can see that at the top of those methods, the object id matches the one you passed in; they're referencing the same object.

So, in pop_arr, when you call delete_at, you're operating on the same object that you passed in, and the changes persist after the method returns.

In change_arr, the difference is that you're assigning the internal var to a new object. When you pass in the parameter array, the internal variable references the same object you passed in. When you instantiate a new Array object and assign the internal array variable to it, the internal variable is now referencing a different object.

def change_arr array
  puts "change id: #{array.object_id}"
  array = [4, 5, 6]
  puts "change id2: #{array.object_id}"
  array
end

That's why the changes don't persist after the method ends. If you wanted the changes to persist, you'd have to say

array = change_arr(array)

Hope that helps.

Community
  • 1
  • 1
Matt
  • 311
  • 1
  • 7