Some basics of variable scope in Ruby

What is variable scope? According to David Black in The Well-Grounded Rubyist, variable scope

refers to the the reach or visibility of identifiers, specifically variables and constants. Different types of identifiers have different scoping rules: using, say, the identifier x for a local variable in each of two method definitions has a different effect than using the global variable $x in the same two places, because local and global variables differ as to scope.

So, when we ask for the value of some variable x, the answer we get depends not only on where we originally defined x, but also on where we are when we are asking for x. This confused me at first, but after learning more about variable scoping, it makes a lot more sense.

Local and global variables

For example, here we define a local variable name and a global variable $name (the $ indicates that it is a global variable).

name = "Bob" 
$name = "Linda"

Ok, simple enough. To check their values, we can use puts.

puts name #=> Bob
puts $name #=> Linda

No surprises there. We can easily assign a new value to name, too, and again check that the value has changed.

name = "Gene"
puts name #=> Gene 

Here’s where things get a little more interesting. Here’s a method name_changer that, when given a string new_name, will assign the value of new_name to both name and $name. From within the method, puts prints the value of each variable before and after the assignments.

def name_changer(new_name)
    puts "new_name is #{new_name}"
    name = new_name
    puts "now the local variable name is #{name} and new_name is #{new_name}"
    puts "global $name is #{$name}"  
    $name = new_name
    puts "now the global variable $name is #{$name} and new_name is #{new_name}"  
end

Here’s what happens when we use this method to change name and $name to Tina.

name_changer("Tina")

# => new_name is Tina
#    now the local variable name is Tina and new_name is Tina
#    global $name is Linda
#    now the global variable $name is Tina and new_name is Tina

Success, right? From within the method, it looks like both name and $name have been changed to Tina. But if we check them again outside of the method

puts name #=> Gene
puts $name #=> Tina

… only the global variable $name was changed! The local variable name wasn’t updated after all. Global variables have global scope, meaning that they can be accessed from anywhere (inside or outside of a method, for example). In contrast, our local variable can only be accessed and changed locally. The name within the method, along with any changes made to it within that method, is only accessible within that method call.

It’s a little confusing that variables with the same name can be so independent of each other. However, this actually protects us from accidently writing over or changing variables from various parts of our program. If you wanted a variable to be easily changeable from anywhere, you could use a global variable instead, but I have never, ever, ever seen any recommendation for doing so.

But what if you did want to use a single variable to keep track of some value, and that value needed to be updated from multiple places? There’s probably a better way to do that than using global variables, and one of these ways becomes obvious from the distinction between instance and class variables, and their relative scopes.

Instance and class variables

Here’s snippet of code defining a Dog class, modified from an earlier post about Ruby classes.

class Dog

    attr_reader :name, :friendliness
    @@dog_count = 0 

    def initialize(name)
        @name = name
        @friendliness = rand(1..10)
        @@dog_count += 1
    end

    def profile
        puts "name: #{self.name}"
        puts "friendliness: #{self.friendliness}"
    end

    def dog_count
        puts "There are #{@@dog_count} dogs in the universe!"
    end
end

Every new instance of the Dog class is initialized with a name and a random friendliness score between 1 and 10. The single @ symbol, as in @name indicates that this is in instance variable. As such, the scope of this variable is limited to this specific instance of Dog. This means every individual dog can have its own name (and its own friendliness score).

# make some dogs!
fido = Dog.new(name="Fido")          
pluto = Dog.new(name="Pluto") 

# Check the value of @name in each instance of Dog
fido.name #=> Fido
pluto.name #=> Pluto 

# These don't interfere with our original local variable, name, either...
puts name #=> Gene

But let’s say we also wanted to keep track of how many dogs have been created. We could do this with a global variable, updating each time a new Dog object is created, but there is a better way: class variables. Class variables are identified by the double @ symbols, as in @@dog_count in the initialize block.

Unlike instance variables, which are only accessible by their corresponding instance of Dog, class variables are accessible and shared by all Dogs. Every time a new Dog is created, @@dog_count is incremented by 1.

fido.dog_count #=> 2
pluto.dog_count #=> 2

# Making a new dog updates @@dog_count by 1

mia = Dog.new(name="Mia")
mia.dog_count #=> 3

# this can be accessed from any dog

fido.dog_count #=> 3
pluto.dog_count #=> 3

So class variables have a wide enough scope to allow us to keep track of all of our dogs, without being so accessible that they can just be changed from anywhere (like a global variable). They give just the right level of access to just the right kind of objects.

Understanding differences in variable scope across local, global, instance, and class variables will help you maintain sanity when debugging your code and protect you from accidental changes. For more on variable scope in Ruby, check out these resources: