Understanding Scope in Ruby

Share this article

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
end
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
  'hello'
end

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
some_class.some_method

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
  end
end

some_class = SomeClass.new
some_class.some_method

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
end

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'
end
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
end

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} "
end

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 }
end

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 }
end

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
end

p i # 0
a_lambda.call
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
  end

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

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'
  end

  def some_method
    sleep 1000
    a = 'hello'
  end
end

some_object = SomeClass.new
some_object.some_method

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!

Frequently Asked Questions (FAQs) about Understanding Scope in Ruby

What is the difference between local and global scope in Ruby?

In Ruby, a local scope refers to the area within a program where a variable is accessible. This could be within a method, a loop, or a block. Once you exit that particular method, loop, or block, the variable is no longer accessible. On the other hand, a global scope refers to variables that are accessible from anywhere in the program. These variables are declared by starting with a dollar sign ($). While they offer convenience, global variables can lead to code that is difficult to debug and maintain, so their use is generally discouraged in Ruby.

How does scope work in Ruby on Rails?

In Ruby on Rails, scope is used to specify commonly-used queries that can be referenced as method calls in the model. This can help to keep your code DRY (Don’t Repeat Yourself) and readable. You can define a scope in your model with the scope method, passing it a name and a lambda function that contains the query you want to run.

What is the role of instance variables in Ruby’s scope?

Instance variables in Ruby have a scope that is tied to the current object. They are accessible from any method within the same object. This means that you can set an instance variable in one method, and then access or modify it in another method within the same object. Instance variables are declared by starting with an at sign (@).

How does constant scope work in Ruby?

In Ruby, constants have a different scope compared to variables. They are accessible within the class or module in which they are defined, as well as from any subclasses or included modules. However, they are not accessible outside of these areas. Constants in Ruby are declared by starting with an uppercase letter.

What is the difference between class variables and class instance variables in Ruby?

Class variables in Ruby are shared among the class and all of its descendants. They are declared by starting with two at signs (@@). On the other hand, class instance variables are tied to a specific instance of a class, not to the class itself or any of its descendants. They are declared by starting with a single at sign (@) and are typically used to store instance-level state for a class.

How can I change the scope of a variable in Ruby?

The scope of a variable in Ruby is determined by where and how it is declared. For example, if you declare a variable within a method, it will have a local scope and will only be accessible within that method. If you want to make a variable accessible from anywhere in your program, you can declare it as a global variable by starting with a dollar sign ($).

What is the purpose of self in Ruby’s scope?

In Ruby, self is a special variable that refers to the current object. It is used to access the current object’s methods and instance variables. The value of self changes depending on the context, but it always refers to the current object.

How does scope affect methods in Ruby?

In Ruby, methods have their own local scope. This means that any variables declared within a method are not accessible outside of that method. However, methods can access instance variables and class variables, as these have a wider scope.

What is the difference between public, private, and protected methods in Ruby’s scope?

In Ruby, public methods can be called by any object. Private methods can only be called within the context of the current object (i.e., they can’t be called with an explicit receiver). Protected methods can be called by any instance of the defining class or its subclasses. Whether a method is public, private, or protected in Ruby affects its visibility, which is a different concept from its scope.

How does nesting affect scope in Ruby?

In Ruby, nesting can affect the scope of constants and classes. If a class or module is defined within another class or module, it is nested and its scope is limited to the enclosing class or module. This means that it can only be accessed from within the class or module in which it is nested.

Darko GjorgjievskiDarko Gjorgjievski
View Author

Darko is a back-end devleoper who enjoys working with Ruby & discovering and learning new things about the language every day.

GlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form