Table of contentsClick link to navigate to the desired location
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.