My take on the world of coding

Variable Scope in Ruby

Interesting? Confusing? What’s the word…

This week in my Intro to Ruby class, we learned about variable scope. I thought I had a fairly good grasp on the concept of “Pass-by-reference-of-the-value”, which is what I believe my teacher called Ruby’s implementation of variable passing. Was I wrong?

The gist of it

Here is a basic synopsis of the Ruby variable scope as I understand it. Any variables, excluding instance and global variables, defined in a scope are not available in any scope larger than its own. I am not sure if that is the right way to say it, but here is an example.

1
2
3
4
5
6
7
8
9
10
def my_method
  do_some_stuff

  if i_did_the_stuff
    the_stuff_is_done = true
  end

  puts = the_stuff_is_done
  # => NameError: undefined local variable or method
end

In this example, my_method is a super-scope (I think that is the right name for it) of the i_did_the_stuff if-block. Thus, the_stuff_is_done is unavailable outside of the if-block where it is defined.

This rule has also an inverse. Any variable defined is accessible from if-blocks and do/end sub-scopes, but not def/end sub-scopes.

Here is what I mean.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
start_script = true

if 'apple'.contains?('a')
  puts start_script
  # => true
end

1.times do
  puts start_script
  # => true
end

# code runs twice
(1..2).each do |num|
  puts "#{num}: #{start_script}"
  # => 1: true
  # => 2: true
end

def my_method
  puts start_script
  # => NameError: undefined local variable or method
end

How do I access my variable from inside of a def/end (method)?

I am glad you asked! The variable must be passed in to the method as a parameter.

1
2
3
4
5
def do_something(with_these)
  recreate_stonehenge(with_these)
end

do_something(my_rocks)

If I was a super-amazing programmer I might know how to turn those four lines of code into a stonehenge recreation, but for now I guess I will have to settle for the mental picture. As you can see, the application gave the my_rocks object to the do_something method to use. The do_something method then assigns that object (parameter) a new name (variable shadowing is generally a bad idea, don’t use the same name for the variable inside of the method as outside). This new name is only usable inside of the do_something method scope.

When you pass a parameter to a method, you are actually passing a reference to the value. Basically, the method receives the value and assigns that value to the parameter-named variable inside of the new scope. You can use the variable all you want, and when you exit the method scope…you are back to the variable and value that you passed in to the method.

Except when…

There always has to be an exception, doesn’t there? The only time that the method-scope affects the variable in the super-scope is when the object is mutated, meaning changed, in place. This mutation is usually done with a bang-method, but not always. Methods like Array#pop and Array#push are mutating methods as well. Here are some examples.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
my_favorite_things = ['Writing code', 'Off-roading (rock crawling)', 'Eating dinner in front of the tv']

def sort_my_favorites(array)
  p array.sort
  # => ["Eating dinner in front of the tv", "Off-roading (rock crawling)", "Writing code"]
end

sort_my_favorites(my_favorite_things)

p my_favorite_things
# => ["Writing code", "Off-roading (rock crawling)", "Eating dinner in front of the tv"]

def permanently_sort_my_favorites(array)
  p array.sort!
  # => ["Eating dinner in front of the tv", "Off-roading (rock crawling)", "Writing code"]
end

permanently_sort_my_favorites(my_favorite_things)

p my_favorite_things
# => ["Eating dinner in front of the tv", "Off-roading (rock crawling)", "Writing code"]

As you can see, the my_favorite_things array was affected by the permanently_sort_my_favorites method, because the method mutated the parameter (variable) directly.

What I found tonight

I found something interesting tonight on accident, while writing code for the exercises in Learn to Program for my class.

The exercise asks the reader to write their own sort method. Here is what I did.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def sort(array_to_sort)
  sorted_array = []
  unsorted_array = array_to_sort

  while unsorted_array.size > 0
    largest_item = 0

    (0..unsorted_array.size-1).each do |index|
      if unsorted_array[index] < unsorted_array[largest_item]
        largest_item = index
      end
    end

    sorted_array << unsorted_array.delete_at(largest_item)
  end

  sorted_array
end

Here is the execution and end-result of the array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
words = ['I', 'am', 'sitting', 'at', 'The', 'mall', 'in', 'the', 'food',
         'court', 'waiting', 'Waiting', 'for', 'my', 'girlfriend']

puts 'Result from sort:'
puts sort(words)
# => I
     The
     Waiting
     am
     at
     court
     food
     for
     girlfriend
     in
     mall
     my
     sitting
     the
     waiting

p words
=> []

Huh? Empty?!? I assumed that since I was assigning the parameter to a new variable inside of the method, it would not affect the variable in the super scope. I was wrong. The correct way to do this is to “clone” or “duplicate” the value in to a new variable.

1
unsorted_array = array_to_sort.dup

This is definitely one of those gotchas that I will be looking out for in the future. Somehow I doubt that this is the last time I will be finding interesting things about the way that Ruby passes variables.

Comments