CrackedRuby logo

CrackedRuby

Block Syntax and Usage

Overview

Ruby blocks represent executable code segments that can be passed to methods and executed within different contexts. Ruby implements blocks through two distinct syntaxes: the lightweight brace syntax {} for single-line operations and the do...end syntax for multi-line code segments. Methods receive blocks through the yield keyword or by converting them to Proc objects using the & operator.

Blocks operate with lexical scoping, meaning they capture variables from their surrounding environment and maintain access to those variables when executed. Ruby provides three primary mechanisms for working with callable objects: blocks (which cannot be stored in variables), Proc objects (which can be stored and called multiple times), and lambdas (which enforce argument checking and have different return behavior).

# Brace syntax for simple operations
[1, 2, 3].each { |n| puts n * 2 }

# do...end syntax for complex operations
File.open("data.txt") do |file|
  content = file.read
  process_content(content)
end

# Converting blocks to Proc objects
def capture_block(&block)
  stored_proc = block
  stored_proc.call("argument")
end

Basic Usage

Methods receive blocks through the yield keyword, which executes the block with optional arguments. Ruby raises a LocalJumpError if code calls yield without a block present. The block_given? method checks for block presence before yielding.

def process_items(items)
  return enum_for(:process_items, items) unless block_given?
  
  items.each do |item|
    result = yield(item)
    puts "Processed: #{result}"
  end
end

process_items([1, 2, 3]) { |n| n * n }
# Processed: 1
# Processed: 4
# Processed: 9

Block parameters appear between pipe characters and support destructuring for complex data structures. Ruby assigns nil to excess parameters and ignores extra arguments passed to blocks.

data = [["Alice", 30], ["Bob", 25], ["Carol", 35]]

data.each do |name, age|
  puts "#{name} is #{age} years old"
end

# Destructuring with excess parameters
data.each do |name, age, city|
  puts "#{name} (#{age}) from #{city || 'Unknown'}"
end

Ruby's standard library extensively uses blocks for iteration, resource management, and configuration. The each method forms the foundation for most iteration patterns, while map transforms collections and select filters elements based on block return values.

numbers = [1, 2, 3, 4, 5]

# Transform elements
squares = numbers.map { |n| n ** 2 }
# => [1, 4, 9, 16, 25]

# Filter elements
evens = numbers.select { |n| n.even? }
# => [2, 4]

# Resource management
File.open("config.txt") do |file|
  config = file.read
  # File automatically closes when block exits
end

Methods can yield multiple times within a single call, creating powerful iteration and generation patterns. The block executes in the context where it was defined, not where the method calls yield.

def repeat(n)
  n.times do |i|
    yield(i + 1)
  end
end

repeat(3) do |iteration|
  puts "Iteration #{iteration}"
end
# Iteration 1
# Iteration 2  
# Iteration 3

Advanced Usage

Converting blocks to Proc objects using the & parameter enables storage, conditional execution, and passing blocks between methods. The ampersand operator converts blocks to Proc objects and Proc objects back to blocks.

class EventHandler
  def initialize(&callback)
    @callback = callback
  end
  
  def handle_event(data)
    @callback&.call(data) if @callback
  end
  
  def chain_handler(&additional)
    original = @callback
    @callback = proc do |data|
      original&.call(data)
      additional&.call(data)
    end
  end
end

handler = EventHandler.new { |data| puts "Primary: #{data}" }
handler.chain_handler { |data| puts "Secondary: #{data}" }
handler.handle_event("test")
# Primary: test
# Secondary: test

Proc objects and lambdas differ significantly in argument handling and return behavior. Lambdas enforce argument count matching and return from the lambda itself, while Proc objects ignore extra arguments and return from the enclosing method.

def demonstrate_returns
  p = proc { return "from proc" }
  l = lambda { return "from lambda" }
  
  result = p.call  # This returns from demonstrate_returns
  puts "After proc: #{result}"  # Never executes
end

def demonstrate_lambda_returns
  l = lambda { return "from lambda" }
  
  result = l.call  # This returns only from lambda
  puts "After lambda: #{result}"  # This executes
  "method return"
end

puts demonstrate_lambda_returns
# After lambda: from lambda
# method return

Block-based DSLs leverage instance_eval and instance_exec to execute blocks in different contexts. This pattern enables configuration objects and builder patterns common in testing frameworks and configuration libraries.

class ConfigBuilder
  def initialize
    @config = {}
  end
  
  def database(&block)
    db_config = DatabaseConfig.new
    db_config.instance_eval(&block)
    @config[:database] = db_config.to_h
  end
  
  def self.build(&block)
    builder = new
    builder.instance_eval(&block)
    builder.to_h
  end
  
  def to_h
    @config
  end
end

class DatabaseConfig
  def initialize
    @settings = {}
  end
  
  def host(value)
    @settings[:host] = value
  end
  
  def port(value)
    @settings[:port] = value
  end
  
  def to_h
    @settings
  end
end

config = ConfigBuilder.build do
  database do
    host "localhost"
    port 5432
  end
end

Method chaining with blocks creates fluent interfaces for data processing pipelines. Each method returns an object that accepts further method calls, building complex operations from simple components.

class DataPipeline
  def initialize(data)
    @data = data
  end
  
  def filter(&block)
    DataPipeline.new(@data.select(&block))
  end
  
  def transform(&block)
    DataPipeline.new(@data.map(&block))
  end
  
  def reduce(initial = nil, &block)
    if initial
      @data.reduce(initial, &block)
    else
      @data.reduce(&block)
    end
  end
  
  def to_a
    @data
  end
end

result = DataPipeline.new([1, 2, 3, 4, 5, 6])
  .filter { |n| n.even? }
  .transform { |n| n * n }
  .reduce(:+)
# => 56 (4 + 16 + 36)

Common Pitfalls

Variable scope in blocks frequently causes confusion because blocks capture variables by reference, not value. Changes to captured variables affect all references to those variables.

# Problematic: All blocks reference the same variable
actions = []
(1..3).each do |i|
  actions << proc { puts i }
end

actions.each(&:call)
# 3
# 3  
# 3

# Correct: Create new scope for each iteration
actions = []
(1..3).each do |i|
  actions << proc { |captured_i| puts captured_i }.curry[i]
end

# Alternative: Use block parameters to create new scope
actions = []
(1..3).each do |i|
  actions << lambda do |captured_i|
    puts captured_i
  end.curry[i]
end

Return statements in blocks behave differently based on block type. Proc objects return from the enclosing method, potentially causing unexpected control flow, while lambdas return only from themselves.

def test_proc_return
  [1, 2, 3].each do |n|
    return "early exit" if n == 2  # Returns from test_proc_return
  end
  "normal exit"
end

def test_lambda_return
  callback = lambda do |n|
    return "lambda exit" if n == 2  # Returns only from lambda
  end
  
  [1, 2, 3].each(&callback)
  "normal exit"
end

puts test_proc_return   # "early exit"
puts test_lambda_return # "normal exit"

Symbol-to-proc conversion using &:method_name works only with methods that accept no arguments. Attempting to use this syntax with methods requiring arguments raises errors or produces unexpected results.

words = ["hello", "world"]

# Correct: No arguments needed
lengths = words.map(&:length)
# => [5, 5]

# Incorrect: gsub requires arguments
# This doesn't work as expected
cleaned = words.map(&:upcase)  # Works
# => ["HELLO", "WORLD"]

# But this fails
# cleaned = words.map(&:gsub)  # ArgumentError

# Correct approach for methods with arguments
cleaned = words.map { |word| word.gsub(/l/, 'x') }
# => ["hexxo", "worxd"]

Block syntax precedence can create parsing ambiguity when method calls chain with blocks. Brace syntax binds more tightly than do...end, leading to different method call associations.

# These produce different results
puts [1, 2, 3].map { |n| n * 2 }  # Passes block to map
# 2
# 4  
# 6

puts [1, 2, 3].map do |n|  # Passes block to puts
  n * 2
end
# [2, 4, 6]

# Use parentheses to clarify intent
puts([1, 2, 3].map { |n| n * 2 })
# 2
# 4
# 6

puts([1, 2, 3].map do |n|
  n * 2  
end)
# 2
# 4
# 6

Performance implications arise when blocks create excessive object allocations. Each block parameter creates new local variables, and complex blocks executed frequently can impact memory usage.

# Inefficient: Creates many intermediate strings
large_array = (1..100_000).to_a
result = large_array.map do |n|
  "Number: #{n}".upcase.strip
end

# More efficient: Minimize object creation
result = large_array.map do |n|
  "NUMBER: #{n}"
end

# Consider lazy evaluation for large datasets
result = large_array.lazy
  .map { |n| "NUMBER: #{n}" }
  .select { |s| s.include?("5") }
  .first(10)

Reference

Core Block Methods

Method Parameters Returns Description
yield *args Object Executes block with arguments
yield *args, **kwargs Object Executes block with args and keywords
block_given? None Boolean Checks if block provided to method
proc { } Block Proc Creates Proc object from block
lambda { } Block Proc Creates lambda from block
Proc.new Block Proc Creates Proc object from block

Proc Object Methods

Method Parameters Returns Description
#call(*args) *args Object Executes proc with arguments
#[](*args) *args Object Alias for call
#===(*args) *args Object Alias for call (case equality)
#arity None Integer Number of required parameters
#lambda? None Boolean Returns true if proc is lambda
#curry Integer (optional) Proc Returns curried version
#source_location None Array File and line where defined
#parameters None Array Parameter information

Block Conversion Operators

Operator Usage Description
&block Method parameter Converts block to Proc object
&proc Method argument Converts Proc to block
&:symbol Method argument Converts symbol to proc via to_proc

Common Iterator Methods with Blocks

Method Block Parameters Returns Description
each element Enumerable Iterates over elements
map element Array Transforms elements
select element Array Filters elements (truthy)
reject element Array Filters elements (falsy)
find element Object/nil Returns first match
reduce accumulator, element Object Reduces to single value
each_with_index element, index Enumerable Iterates with index
each_with_object element, object Object Iterates with object

Block Syntax Rules

Context Syntax Usage
Single line { |params| code } Simple operations
Multi-line do |params|..end Complex operations
No parameters { code } or do..end Parameter-less blocks
Multiple parameters { |a, b, c| code } Destructuring assignment
Splat parameters { |*args| code } Variable arguments
Keyword parameters { |**kwargs| code } Keyword arguments

Error Types

Exception Cause Prevention
LocalJumpError yield without block Use block_given?
ArgumentError Wrong argument count (lambdas) Match parameter count
NoMethodError Symbol to_proc on invalid method Verify method exists

Performance Characteristics

Pattern Memory Impact CPU Impact Recommendation
Simple blocks Low Low Preferred for simple operations
Proc objects Medium Low Good for reusable code
Lambda objects Medium Low Use for strict argument checking
Instance_eval blocks High Medium Limit to DSL contexts
Nested blocks Medium Medium Avoid deep nesting