CrackedRuby logo

CrackedRuby

Variable Scope and Binding

Overview

Variable scope determines where variables can be accessed within a Ruby program. Ruby uses lexical scoping combined with dynamic binding to create predictable yet flexible variable access patterns. The language defines four primary variable types based on their scope and naming conventions: local variables, instance variables, class variables, and global variables.

Ruby's Binding class captures the execution context at a specific point, preserving variable scope and method definitions. This binding object contains references to local variables, instance variables, and the current self object. The binding mechanism enables metaprogramming techniques like eval, instance_eval, and class_eval to execute code within captured contexts.

def capture_context
  local_var = "captured"
  binding
end

context = capture_context
context.eval("local_var")  # => "captured"

Local variables exist within method definitions, blocks, and class/module bodies. Their scope follows lexical boundaries, ending at method returns or block termination. Instance variables belong to specific object instances and remain accessible throughout the object's lifetime. Class variables are shared among all instances of a class and its subclasses, while global variables maintain program-wide accessibility.

class ScopeExample
  @@class_var = "shared"
  
  def initialize
    @instance_var = "per object"
  end
  
  def method_scope
    local_var = "method only"
    [local_var, @instance_var, @@class_var]
  end
end

Ruby creates new binding objects automatically when entering methods, blocks, or eval contexts. These bindings maintain references to variables accessible at creation time, forming closures that preserve state even after their lexical scope ends.

Basic Usage

Local variables use lowercase or underscore-prefixed names and exist within their defining scope. Ruby determines local variable scope at parse time, creating variables when assignments appear in the source code regardless of execution flow.

def variable_scope_demo
  if false
    unreachable = "never executed"
  end
  
  puts unreachable  # => nil (variable exists, value is nil)
end

Method parameters create local variables within the method scope. Block parameters create new local variables that shadow outer variables with the same name within the block.

outer = "original"

[1, 2, 3].each do |outer|
  puts outer  # => 1, 2, 3 (shadows outer variable)
end

puts outer  # => "original" (unchanged)

Instance variables begin with @ and belong to the current object referenced by self. Ruby creates instance variables on first assignment and returns nil for undefined instance variables.

class InstanceVariableExample
  def set_var
    @name = "Ruby"
  end
  
  def get_var
    @name  # Returns value or nil
  end
  
  def undefined_var
    @undefined  # => nil
  end
end

Class variables start with @@ and are shared across all instances of a class and its subclasses. Ruby raises NameError when accessing undefined class variables.

class Counter
  @@count = 0
  
  def increment
    @@count += 1
  end
  
  def current_count
    @@count
  end
end

c1, c2 = Counter.new, Counter.new
c1.increment
c2.current_count  # => 1

The binding method returns a Binding object containing the current execution context. This object preserves local variables, instance variables, and the current self reference.

def create_binding
  name = "context"
  age = 25
  binding
end

context = create_binding
context.local_variables  # => [:name, :age]
context.eval("name")     # => "context"

Advanced Usage

Blocks create closures that capture variables from their defining scope. These closures maintain references to outer variables even after the defining method returns, enabling sophisticated functional programming patterns.

def create_counter(initial = 0)
  count = initial
  
  -> { count += 1 }
end

counter1 = create_counter(10)
counter2 = create_counter

counter1.call  # => 11
counter1.call  # => 12
counter2.call  # => 1

Binding objects support method chaining for context evaluation. The eval method executes strings within the binding's context, while local_variable_get and local_variable_set provide direct variable access.

def advanced_binding_demo
  secret = "hidden value"
  public_data = "visible"
  
  context = binding
  context.local_variable_set(:secret, "modified")
  context.eval("secret + ' and ' + public_data")
end

advanced_binding_demo  # => "modified and visible"

Ruby's scope gates (class, module, and def keywords) create new variable scopes that cannot access local variables from outer scopes. This behavior differs from block scoping where outer variables remain accessible.

local_var = "outer"

class ScopeGate
  # local_var is not accessible here
  
  def method_definition
    # local_var is not accessible here
    inner_var = "method scope"
  end
  
  [1, 2, 3].each do |n|
    # local_var IS accessible in blocks
    puts "#{local_var}: #{n}"
  end
end

The Proc and lambda objects capture their creation context as bindings. This capture includes local variables, creating true closures with persistent state.

class ClosureFactory
  def create_proc(multiplier)
    base = 10
    
    Proc.new do |value|
      result = base * multiplier * value
      base += 1  # Modifies captured variable
      result
    end
  end
end

factory = ClosureFactory.new
proc_obj = factory.create_proc(3)

proc_obj.call(2)  # => 60 (10 * 3 * 2, base becomes 11)
proc_obj.call(2)  # => 66 (11 * 3 * 2, base becomes 12)

Method objects created with Object#method capture the receiver's binding, preserving instance variable access even when called from different contexts.

class BindingPreservation
  def initialize(value)
    @value = value
  end
  
  def get_value
    @value
  end
  
  def modify_value(new_value)
    @value = new_value
  end
end

obj = BindingPreservation.new("original")
getter = obj.method(:get_value)
setter = obj.method(:modify_value)

# Methods preserve original object binding
setter.call("modified")
getter.call  # => "modified"

Common Pitfalls

Variable shadowing occurs when inner scopes define variables with the same names as outer variables. Ruby prioritizes the innermost scope, potentially hiding intended variables.

name = "outer"

def confusing_shadowing
  name = "method"
  
  [1, 2, 3].each do |item|
    name = "block #{item}"
    puts name
  end
  
  puts name  # => "block 3" (method variable was overwritten)
end

Local variable creation happens at parse time, not execution time. Ruby creates local variable slots for any assignment statement it encounters, even within unreachable code paths.

def parse_time_confusion
  puts defined?(variable)  # => "local-variable"
  
  if false
    variable = "never assigned"
  end
  
  puts variable  # => nil (exists but uninitialized)
end

Class variable inheritance creates unexpected sharing between parent and child classes. Modifications in subclasses affect the parent class and all other subclasses.

class Parent
  @@shared = "parent value"
  
  def self.get_shared
    @@shared
  end
  
  def self.set_shared(value)
    @@shared = value
  end
end

class Child < Parent
end

Child.set_shared("modified by child")
Parent.get_shared  # => "modified by child"

Block variable scoping with parameters creates new variables that shadow outer variables, but block-local variables (Ruby 1.9+) can prevent this shadowing.

outer = "preserved"

# This shadows the outer variable
[1].each do |outer|
  outer = "modified"
end
puts outer  # => "preserved"

# Block-local variables prevent confusion
[1].each do |item; outer|
  outer = "block local"
  puts outer  # => "block local"
end
puts outer  # => "preserved"

Binding objects maintain references to their creation context, potentially causing memory leaks if large objects remain referenced through captured variables.

def memory_leak_example
  large_data = Array.new(1_000_000) { rand }
  
  # This binding keeps large_data alive
  -> { "closure created" }
end

# large_data cannot be garbage collected
closures = Array.new(100) { memory_leak_example }

The eval method within bindings can access and modify captured variables, but string evaluation bypasses Ruby's normal variable scope checking.

def dangerous_eval
  password = "secret123"
  user_input = "password = 'hacked'; password"
  
  context = binding
  context.eval(user_input)  # => "hacked" (password was modified)
end

Constants have lexical scope but follow different resolution rules than variables. Ruby searches constant names through the inheritance hierarchy and lexical nesting.

module Outer
  CONSTANT = "outer"
  
  module Inner
    CONSTANT = "inner"
    
    def self.access_constant
      CONSTANT  # => "inner" (lexical scope)
    end
    
    def self.access_outer
      Outer::CONSTANT  # => "outer" (explicit qualification)
    end
  end
end

Reference

Variable Types and Scope Rules

Variable Type Syntax Scope Inheritance Access Pattern
Local variable Lexical block No Direct name
Instance @variable Object instance No Through object
Class @@variable Class hierarchy Yes Shared across instances
Global $variable Program-wide N/A Global access

Binding Class Methods

Method Parameters Returns Description
#eval(string, filename=nil, lineno=1) string (String), optional location Object Evaluates string in binding context
#local_variables None Array<Symbol> Returns local variable names
#local_variable_get(symbol) symbol (Symbol) Object Gets local variable value
#local_variable_set(symbol, obj) symbol (Symbol), obj (Object) Object Sets local variable value
#local_variable_defined?(symbol) symbol (Symbol) Boolean Checks if local variable exists
#receiver None Object Returns the bound object (self)

Scope Gate Keywords

Keyword Creates New Scope Access to Outer Variables Usage Context
class Yes No Class definition
module Yes No Module definition
def Yes No Method definition
lambda No (closure) Yes Lambda creation
proc No (closure) Yes Proc creation
Block No (closure) Yes Block execution

Variable Resolution Order

  1. Local variables - Current scope, then outer lexical scopes (blocks only)
  2. Method calls - Current object, then method lookup chain
  3. Constants - Lexical scope, then inheritance hierarchy, then top-level

Binding Creation Points

Context Binding Contains Variable Access
Method call Method parameters, local variables, self Instance variables, class variables
Block execution Block parameters, outer variables, self Closure over creation scope
eval execution String evaluation context Context-dependent access
Class/Module body Class-level context, constants Class instance variables

Common Binding Patterns

# Capture current context
context = binding

# Evaluate string in context
context.eval("local_variable")

# Access binding variables
context.local_variable_get(:name)
context.local_variable_set(:name, "new value")

# Check variable existence
context.local_variable_defined?(:name)

Variable Naming Conventions

Pattern Variable Type Example Scope
[a-z_][a-zA-Z0-9_]* Local user_name Lexical
@[a-zA-Z_][a-zA-Z0-9_]* Instance @name Object
@@[a-zA-Z_][a-zA-Z0-9_]* Class @@count Class hierarchy
$[a-zA-Z_][a-zA-Z0-9_]* Global $global_var Program
[A-Z][A-Z0-9_]* Constant CONSTANT Lexical + inheritance