🤯 New: Coding Assessments Practice your skills on real-world programming challenges

Understanding Scope in Ruby

Darko Gjorgjievski

SCOPE Overlapping Letters Vector Icon

Scope is an essential thing to understand, not just in Ruby, but in any programming language. Back in the days when I was starting to write Ruby, more than half of the errors thrown at me were as a result of not understanding this concept well enough. Things like variables not being defined, incorrect variable assignments, and so on. All as a result of not understanding scope well enough. You don’t have to go through all of these headaches! This article will hopefully help you avoid many of the mistakes I did.

What is Scope, Exactly?

When someone mentions scope, think of 2 words: variables and visibility.. It’s pretty straightforward: Scope is all about where something is visible. It’s all about what (variables, constants, methods) is available to you at a given moment. If you understand scope well enough, you should be able to point at any line of your Ruby program and tell which variables are available in that context, and more importantly, which ones are not.

So what’s the point of restricting visibility to variables in the first place? Why not have everything available anywhere? Life would be much easier that way, wouldn’t it? Well, not really…

There are many things programmers disagree about (using a functional vs. object-oriented approach, using different variable naming templates, etc.). Scope is not one of them. Especially as people get more experience with programming, they are stronger proponents of it. Why is that?

Because the more you work in programming, the more you’ll have the chance to experience the horror of having everything available everywhere. Global state makes your program very unpredictable. It’s very hard to track who changes what when everyone has the ability to do so. Who wrote to this variable? Who read it? Imagine having ten thousand lines of code and asking these questions for each line!

Then come naming issues. If you have a big program, you’d have to give all of your variables unique names to avoid conflicts. Imagine keeping track of thousands of variable names.

Scope is like another principle used in computer security: The Principle of Least Access. Imagine if everyone in a bank (the teller, the accountant) had (read and write) access to every record on customers, financials, and so on. Suddenly, someone changes a client’s balance. Was it the teller or the accountant? Maybe they did it both? You get the idea.

Ruby Variable Scope: A Quick Reference

Ruby has variables defined within different scopes, which you probably know already. I found that most tutorials describe them briefly (the variable types), but they fail to mention precisely what their scope is. Here are the details:

  • Class variable (@@a_variable): Available from the class definition and any sub-classes. Not available from anywhere outside.
  • Instance variable (@a_variable): Available only within a specific object, across all methods in a class instance. Not available directly from class definitions.
  • Global variable ($a_variable): Available everywhere within your Ruby script.
  • Local variable (a_variable): It depends on the scope. You’ll be working with these most and thus encounter the most problems, because their scope depends on various things.

Here’s a nice illustration to visualize the visibility of all 4 of them. It should give you a brief idea of what is their range and availability:

I’ll focus on local variables through the next few sections. I find, though my experience and talking with people, most of the problems with scope come from not knowing enough about local variables and how they function.

When is a Local Variable in Scope?

You don’t need to declare instance variables in Ruby. You can put something like @anything_goes_here in a method definition, and you’ll get nil as a result. Now, try removing that @ at the beginning (thus turning the variable into local) and what you’ll get is a NameError (undefined local variable or method).

The Ruby interpreter will put a local variable in scope whenever it sees it being assigned to something. It doesn’t matter if the code is not executed, the moment the interpreter sees an assignment a local variable, it puts it in scope:

if false # the code below will not run
  a = 'hello' # the interpreter saw this, so the local var. is in scope from now on
p a # nil, since that code didn't execute, thus the variable wasn't initialized

Try running this code as it is and then remove a = 'hello' and run it again to see what happens.

Local Variable Naming Conflicts

Suppose you have this code:

def something

p something
==> hello
something= 'Ruby'
p something
==> Ruby #'hello' is not printed

In Ruby, methods can be called without an explicit receiver and any parentheses, just like local variables. Thus, you can have potential naming conflicts like the one above.

If you have a local variable being initialized and a method call with a same name in the same scope ,the local variable will “shadow” the method and take precedence. That doesn’t mean that the method is gone and cannot be accessed at all. You can easily get access to it by adding either parentheses at the end (with something()) or adding an explicit self receiver before it (with self.something). Take a look:

def some_var; 'I am a method'; end
public :some_var # Because all methods defined at the top level are private by default
some_var = 'I am a variable'
p some_var # I am a variable
p some_var() # I am a method
p self.some_var # I am a method

A Useful Exercise to Detect if a Variable is Out of Scope

If you want to see if a local variable is in scope, first, position your mouse cursor at it. Now, press the left arrow key and go back to your code and stop until you either:

  1. Reach the beginning of your scope (a def/class/module/do-end block)
  2. Reach the code that does the assignment to that local variable.

If you reach 1) before 2), you will probably encounter a NameError in your code. If you reach 2) before 1), then congratulations.

Local vs. Instance Variables

Instance variables are associated with a particular object. As long as you’re in that object, you have access to them. Local variables, unlike instance variables, are associated with a particular scope. As long as you’re in that scope, you’ll have access to them. Instance variables change and get replaced with every new object. Local variables change and get replaced with every new scope. How do you know if a scope is changed? Two words: scope gates.

Scope Gates: An Essential Concept to Understanding Scope

What do you think happens when you:

  1. Define a class (with class SomeClass)
  2. Define a module (with module SomeModule)
  3. Define a method (with def some_method)?

Every time you do any of these three things, you enter a new scope. It’s like Ruby opening a gate for you and taking you to an entirely different context with entirely different variables.

Every method/module/class definition is known as a scope gate, because a new scope is created. The old scope is no longer available to you, and all variables available in it are replaced with the new ones.

If this is confusing, don’t worry. Here’s an example to help you out better grasp the concept:

v0 = 0
class SomeClass # Scope gate
  v1 = 1
  p local_variables # As the name says, it gives you all local variables in scope

  def some_method # Scope gate
    v2 = 2
    p local_variables
  end # end of def scope gate
end # end of class scope gate

some_class = SomeClass.new

You’ll see [:v1] and [:v2] being printed to the console. What happened to the v1 variable when the program got into def some_method? The class scope got replaced with the instance method scope, introducing a total new set of variables into the mix (in this case, it’s just one).

What about v0? It didn’t appear anywhere! Well, the moment we entered into class SomeClass, that top-level scope (v0 is defined in the top-level) got replaced as well. When I say “replaced”, I mean temporarily, not permanently. Try adding p local_variables at the end of this program after some_class.some_method and you’ll see v0 right there in the list.

Break the Scope Gates!

Module/method/class definitions, as we’ve seen previously, limit the visibility of variables. If you have local variables in a class, when a new method is defined in the class the local variables in the class are no longer available, as we’ve seen. What if you wanted to still have access to these variables despite the method definition? How can you “break” those scope gates?

The key to this is simple: Replace scope gates with method calls. More specifically, replace:

  • class definitions with with Class.new
  • module definitions with Module.new
  • method definitions with define_method

Let’s take the above code (the variable names, everything is the same) and just replace the scopes gates with method calls:

v0 = 0
SomeClass = Class.new do
  v1 = 1
  p local_variables

  define_method(:some_method) do
    v2 = 2
    p local_variables

some_class = SomeClass.new

After you run this, you’ll see 2 lines printed: [:v1, :v0, :some_class] and [:v2, :v1, :v0, :some_class]. We successfully broke each and every scope gate and made the outside variables available. This result was possible through the power of blocks, which we’ll explore below.

Are Blocks Scope Gates?

You may think that blocks are scope gates as well. After all, they do introduce a new scope with variables declared inside them, which you can’t access outside, as with this example:

sample_list = [1,2,3]
hi = '123'
sample_list.each do |item| # the block scope begins here
  puts hi # will this print 123 or produce an error?
  hello = 'hello' # declaring and initializing a variable

p hello # undefined local variable or method "hello"

As you can see with “hello”, variables inside a particular block are local to that block and are not available anywhere else.

If blocks were scope gates, then puts hi would produce an error because the hi variable is in a separate scope. Yet, this is not the case, and you can see that by running the code above.

Not only can you access outside variables, but change their content as well! Try putting hi = '456' inside do/end and its content will be changed.

What if you don’t want blocks to modify outside variables? Block-local variables can help. To define block-local variables, put a semicolon at the end of the block’s parameters (the block below has only 1 parameter, i) and then just list them:

hi = 'hi'
hello ='hello'
3.times do |i; hi, hello|
  p i
  hi = 'hi again'
  hello = 'hello again'
p hi # "hi"
p hello # "hello"

If you remove the ; hi, hello part, you’ll get “hi again” and “hello again” as the new content of the two variables.

Remember that the moment you start a block with do and end it with end, you are introducing a new scope:

[1,2,3].select do |item| # do is here, new scope is being introduced
  # some code

Replace select with each, map, detect, or any other method. The mere fact a block is being used (once you see do/end) means that a new scope is introduced.

Some Quirks With Blocks and Scopes

Try to guess what will this Ruby code print:

2.times do
  i ||= 1
  print "#{i} "
  i += 1
  print "#{i} "

Did you expect 1 2 2 2? The answer is: 1 2 1 2. Each iteration using times is a new block definition that resets the local variables inside it. In this case, we have 2 iterations, so the moment the second one begins, i is being reset to 1 again.

What do you think this Ruby code will print (the last line):

def foo
  x = 1
  lambda { x }

x = 2

p foo.call

The answer is 1. The reason for this is, blocks and block objects (procs, lambdas) see the scope in their definition and not in their invocation. This has to do with the fact they are treated as closures in Ruby. A closure is simply code containing behavior that can:

  • be passed around like an object (which can be called later)
  • remember the variables that were in scope when the closure (lambda in this example) was defined.

This can come in handy in various cases, like defining an infinite number generator:

def increase_by(i)
  start = 0
  lambda { start += i }

increase = increase_by(3)
start = 453534534 # won't affect anything
p increase.call # 3
p increase.call # 6

You can also delay the modification of a variable using a lambda:

i = 0
a_lambda = lambda do
  i = 3

p i # 0
p i # 3

What do you think the last line will print:

a = 1
ld = lambda { a }
a = 2
p ld.call

If your answer were 1, you’d be wrong. It will print 2. But wait, doesn’t a lambda/proc sees the scope in their definition? That’s true, and if you think about it, a = 2 is also in the definition scope. It isn’t until the first time that lambda is called that it figures out the values of the variables in its definition, as you can see in this example. Not being aware of this fact can potentially introduce potentially hard-to-trace bugs in your code.

How Can Two Methods Share the Same Variable?

Once we know how to break scope gates, we can use this knowledge to do some amazing things. I learned about this concept from the Metaprogramming Ruby book which helped me a lot in understanding how scoping works behind the hood. Anyway, here’s the code:

def let_us_define_methods
  shared_variable = 0

  Kernel.send(:define_method, :increase_var) do
    shared_variable += 1

  Kernel.send(:define_method, :decrease_var) do
    shared_variable -= 1

let_us_define_methods # methods defined now!
p increase_var # 1
p increase_var # 2
p decrease_var # 1

Pretty neat, huh?

The Top-Level Scope

What does it mean to be in the top level scope in Ruby or any other programming language? How do you know when you’re in it? Being at the top level simply means that either you still haven’t called any methods yet, or all your method calls have returned.

In Ruby, everything is an object. Even when you’re at the top-level, you are in an object (called main, belonging to an Object class). Try running the following code to check this for yourself:

p self # main
p self.class # Object

Where am I?

Often, in debugging, you’ll have many of your headaches solved if you know the current value of self. The current value of self affects instance variables as well as methods without an explicit receiver. If you get an error with undefined method/instance variable which you’re sure was defined (because you can see that in your code), then you probably have an issue tracking self.

Small Exercise: What is Available to Me?

Do this small exercise to make sure you understand the fundamental principles in this article. Suppose you’re a little Ruby debugger running the following code:

class SomeClass
  b = 'hello'
  @@m = 'hi'
  def initialize
    @some_var = 1
    c = 'hi'

  def some_method
    sleep 1000
    a = 'hello'

some_object = SomeClass.new

Try to stop at sleep 1000. What do you see? Which variables are available to you at that particular point? Try and come up with some answers before continuing. Your response should not only contain the variables, but also the reasons WHY they are available.

As we’ve mentioned before, local variables are scope-bound. The definition of some_method is a scope gate, replacing all of the previous scope and starting a new one. In the new scope, the a variable is the only local variable that’s available.

Instance variables, as we’ve mentioned previously, are self bound, and in this case, some_object is the current instance, and @some_var is available across all methods that are related to it, including some_method. Class variables are similar and @mm will also be available in the scope. The local variables b and c will be out of reach because of the scope gate. If you want for them to be available everywhere, refer to the section on breaking the scope gates.

Hope you found this article to be useful. If you have any questions, let me know in the comments below!

JavaScript: Novice to Ninja, 2nd Edition