All original content is created in Ukrainian. Not all content has been translated yet. Some posts may only be available in Ukrainian.Learn more

The scope of a local variable in Ruby

Post cover: The scope of a local variable in Ruby
This content has been automatically translated from Ukrainian.
```html
In Ruby, a new scope for a local variable is created in several places. It is necessary to understand and study these places.
  • global context (main);
  • class (or module) definition;
  • method definition;
Let's consider a few examples from the site ruby-lang.org and a few new ones.
We will create a file example.rb with the code

Global Scope (main)

# A global variable is defined outside of any classes, methods, or blocks.
var = 1  # Global variable

class Demo
  var = 2  # Class variable

  def method
    var = 3  # Local method variable
    puts "in method: var = #{var}"
  end

  puts "in class: var = #{var}"
end

puts "at top level: var = #{var}"
Demo.new.method
Running the script 'ruby example.rb' will show us the following result:
# in class: var = 2
# at top level: var = 1
# in method: var = 3
Note that the code inside the class definition is executed during its creation, so the message inside the class appears immediately.

Blocks in Ruby and Their Scope

In Ruby, blocks ({ ... } or do ... end) almost create a new scope. This means that local variables defined inside the block are usually not accessible outside of it. However, there are some nuances. Note the word 'almost', it is used on the Ruby language site, and is sometimes misinterpreted by beginners.

Example with a Block

a = 0
1.upto(3) do |i|
  a += i
  b = i * i
end
puts a  # => 6
puts b  # An error will occur because b is not defined outside the block
In this example, the variable a, which was defined before the block (the do ... end construct), is modified inside the block, and these changes are visible outside the block. On the other hand, the variable b, which was defined inside the block, is not accessible outside of it.
Blocks almost create a new scope because local variables defined inside the block cannot be accessed from outside. However, if a variable already exists in the outer scope before entering the block, it will be accessible inside the block, and changes to it will persist after exiting the block.
Why "almost"? The word "almost" is used for several reasons (which have already been described earlier):
  • Variables defined before the block can be accessed and modified inside the block. Changes to them persist after exiting the block.
  • Variables defined for the first time inside the block are not accessible outside of it.
These nuances are especially important to remember when working with threads and asynchronous code, where the scope can affect the accessibility of variables between different parts of the code.
Let's consider another example for better understanding:
x = 10

[1, 2, 3].each do |i|
  x += i
  y = i * 2
end

puts x  # => 16 (the variable x changes inside the block)
puts y  # An error will occur because y is not defined outside the block
The error for 'puts y' will be as follows:
(irb):8:in `<main>': undefined local variable or method `y' for main:Object (NameError)

puts y
     ^
	from /Users/user/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/irb-1.13.1/exe/irb:9:in `<top (required)>'
	from /Users/user/.rbenv/versions/3.2.1/bin/irb:25:in `load'
	from /Users/user/.rbenv/versions/3.2.1/bin/irb:25:in `<main>'
In this example, the variable x is defined before the block and modified inside the block. The variable y is defined inside the block and is not accessible outside of it. This illustrates how blocks "almost" create a new scope but do not completely isolate access to variables that already exist in the outer scope.

Example with Threads

threads = []

["one", "two"].each do |name|
  threads << Thread.new do
    local_name = name
    a = 0
    3.times do |i|
      Thread.pass
      a += i
      puts "#{local_name}: #{a}"
    end
  end
end

threads.each { |t| t.join }
The result in my case (the result depends on the operation of Thread.pass, the operating system, and the processor):
one: 0
two: 0
one: 1
one: 3
two: 1
two: 3
We create an empty array threads, where we will store all the created threads. The each loop iterates over the array of strings ["one", "two"], where each element will be available in the variable name in turn. For each element of the array, a new thread is created using Thread.new. The code inside the do ... end block will be executed in the context of the new thread.
The variable local_name gets its value from the current element of the array name. This is done so that each thread has its own local copy of the variable name. Next, a local variable a is created with an initial value of 0. The next part is the loop. The loop runs three times. In each iteration, we have the following actions:
  • Thread.pass is executed, which allows other threads to run.
  • The variable a is incremented by the value of i.
  • The value of local_name and the current value of a are printed.
And we finish:
threads.each { |t| t.join }
The join operator forces the main thread to wait for the completion of each of the created threads. This is necessary so that all threads finish their work before the program (script) ends.

Control Structures, Methods, and Their Scopes

Below are examples of control structures and methods for a visual explanation of how scopes work. Ruby has various control structures and methods that allow managing the flow of program execution.
if / elsif / else
if condition
  # code
elsif another_condition
  # other code
else
  # other code
end
unless
unless condition
  # code
end
case / when
case variable
when value1
  # code
when value2
  # other code
else
  # other code
end
while
while condition
  # code
end
until
until condition
  # code
end
for
for element in collection
  # code
end
loop
loop do
  # code
  break if condition
end
begin / rescue / ensure / else
begin
  # code
rescue SomeException => e
  # exception handling
ensure
  # code that always runs
else
  # code that runs if there is no exception
end
redo
for i in 0..5
  retry if i > 2
  puts "i: #{i}"
end
retry
begin
  # code
rescue
  retry
end
next
for i in 0..5
  next if i < 3
  puts "i: #{i}"
end
break
for i in 0..5
  break if i > 2
  puts "i: #{i}"
end
Control structures (if, while, for, etc.) do not create a new scope, so local variables inside them will be accessible in the surrounding environment.
Examples of methods:
times
5.times do |i|
  puts i
end
upto
1.upto(5) do |i|
  puts i
end
downto
5.downto(1) do |i|
  puts i
end
step
0.step(10, 2) do |i|
  puts i
end
each
[1, 2, 3].each do |element|
  puts element
end
map
result = [1, 2, 3].map do |element|
  element * 2
end
puts result
select
result = [1, 2, 3, 4, 5].select do |element|
  element.even?
end
puts result
reject
result = [1, 2, 3, 4, 5].reject do |element|
  element.even?
end
puts result
find
result = [1, 2, 3, 4, 5].find do |element|
  element.even?
end
puts result
inject/reduce
sum = [1, 2, 3, 4, 5].inject(0) do |accumulator, element|
  accumulator + element
end
puts sum
Methods (times, each, etc.) often take blocks that can create a new scope for variables defined inside the block.
I intentionally added many examples of control structures and methods to show that it is easy to make mistakes here and encounter scope-related issues. The main goal is not to memorize all the methods but to remember the difference between control structures and methods. This knowledge will help debug potentially problematic areas in the code.
To visually demonstrate how it all works, let's write tests:
require 'rspec'

RSpec.describe 'Scopes in Ruby' do
  context 'Control Structures' do
    it 'creates a new scope with if/elsif/else' do
      if true
        var = 1
      end
      expect(var).to eq(1)
    end

    it 'creates a new scope with unless' do
      unless false
        var = 2
      end
      expect(var).to eq(2)
    end

    it 'creates a new scope with case/when' do
      case 1
      when 1
        var = 3
      end
      expect(var).to eq(3)
    end

    it 'creates a new scope with while' do
      i = 0
      while i < 1
        var = 4
        i += 1
      end
      expect(var).to eq(4)
    end

    it 'creates a new scope with until' do
      i = 0
      until i > 0
        var = 5
        i += 1
      end
      expect(var).to eq(5)
    end

    it 'creates a new scope with for' do
      for i in 0..0
        var = 6
      end
      expect(var).to eq(6)
    end

    it 'creates a new scope with loop' do
      var = nil
      loop do
        var = 7
        break
      end
      expect(var).to eq(7)
    end

    it 'handles exceptions with begin/rescue/ensure' do
      var = 0
      begin
        raise 'error'
      rescue
        var = 1
      ensure
        var += 2
      end
      expect(var).to eq(3)
    end

    it 'handles exceptions with begin/rescue/else/ensure' do
      var = 0
      begin
        var += 1
      rescue
        var += 2
      else
        var += 3
      ensure
        var += 4
      end
      expect(var).to eq(8)
    end

    it 'repeats execution with redo' do
      var = 0
      i = 0
      for i in 0..5
        if i < 2
          var = i
          break if i == 1 # Avoid infinite loop
        end
      end
      expect(var).to eq(1)
    end

    it 'repeats execution with retry' do
      var = 0
      attempts = 0
      begin
        raise 'error' if attempts < 1
      rescue
        attempts += 1
        retry if attempts < 2
      else
        var = 9
      end
      expect(var).to eq(9)
    end

    it 'skips an iteration with next' do
      var = []
      for i in 0..5
        next if i < 3
        var << i
      end
      expect(var).to eq([3, 4, 5])
    end

    it 'exits the loop with break' do
      for i in 0..5
        break if i > 2
        var = i
      end
      expect(var).to eq(2)
    end
  end

  context 'Methods' do
    it 'creates a new scope with times' do
      1.times do
        var = 10
      end
      expect(defined?(var)).to be_nil
    end

    it 'creates a new scope with upto' do
      1.upto(1) do
        var = 11
      end
      expect(defined?(var)).to be_nil
    end

    it 'creates a new scope with downto' do
      1.downto(1) do
        var = 12
      end
      expect(defined?(var)).to be_nil
    end

    it 'creates a new scope with step' do
      0.step(0, 1) do
        var = 13
      end
      expect(defined?(var)).to be_nil
    end

    it 'creates a new scope with each' do
      [1].each do
        var = 14
      end
      expect(defined?(var)).to be_nil
    end

    it 'creates a new scope with map' do
      [1].map do
        var = 15
      end
      expect(defined?(var)).to be_nil
    end

    it 'creates a new scope with select' do
      [1].select do
        var = 16
      end
      expect(defined?(var)).to be_nil
    end

    it 'creates a new scope with reject' do
      [1].reject do
        var = 17
      end
      expect(defined?(var)).to be_nil
    end

    it 'creates a new scope with find' do
      [1].find do
        var = 18
      end
      expect(defined?(var)).to be_nil
    end

    it 'creates a new scope with inject/reduce' do
      [1].inject(0) do |acc, _|
        var = 19
      end
      expect(defined?(var)).to be_nil
    end
  end
end
All of them executed successfully:
Finished in 0.06456 seconds (files took 0.26825 seconds to load)
23 examples, 0 failures
```

This post doesn't have any additions from the author yet.

29 May 09:09

Which operating systems support Ruby?

meme code
meme code@memecode
Does Ruby create a new copy of an object when assigning a variable to another variable?
29 May 09:30

Does Ruby create a new copy of an object when assigning a variable to another variable?

meme code
meme code@memecode
What is the difference between immediate value and reference in Ruby?
29 May 12:00

What is the difference between immediate value and reference in Ruby?

meme code
meme code@memecode
Why does Ruby code return nil after executing puts?
29 May 20:30

Why does Ruby code return nil after executing puts?

meme code
meme code@memecode
What is the difference between nil and false in Ruby?
29 May 20:59

What is the difference between nil and false in Ruby?

meme code
meme code@memecode
Why is an empty string in Ruby not false?
31 May 14:39

Why is an empty string in Ruby not false?

meme code
meme code@memecode
What is the difference between int and bigint in Ruby? Minimum and maximum values.
13 Jun 06:37

What is the difference between int and bigint in Ruby? Minimum and maximum values.

meme code
meme code@memecode
What does the error 'is out of range' mean in Ruby on Rails? Range Error - Integer with a limit of 4 bytes
13 Jun 07:18

What does the error 'is out of range' mean in Ruby on Rails? Range Error - Integer with a limit of 4 bytes

meme code
meme code@memecode
What is immutability and mutability?
19 Jun 07:48

What is immutability and mutability?

meme code
meme code@memecode
What will be the result of adding 10.5 and 10?
23 Jun 13:23

What will be the result of adding 10.5 and 10?

meme code
meme code@memecode
[Ruby] What is the difference between variables that start with @, @@, and $?
23 Jun 14:00

[Ruby] What is the difference between variables that start with @, @@, and $?

meme code
meme code@memecode
What is a function in programming?
24 Jun 18:15

What is a function in programming?

meme code
meme code@memecode