Blocks and procs
Blocks are Ruby’s distinctive feature: anonymous code attached to a method call, invoked by the called method via yield or captured as a Proc. Almost every iteration in idiomatic Ruby uses blocks — each, map, select, reduce all take blocks. The block syntax is implicit: a block is not part of the method’s argument list; it is attached to the call itself with do...end or { }. For first-class callable values, Ruby admits procs (Proc.new, proc { }) and lambdas (lambda { }, -> { }), with subtly different semantics. The combination — implicit blocks attached to calls, the yield mechanism for invoking them, procs and lambdas as first-class objects, the symbol-to-proc shorthand (&:method) — is the substance of Ruby’s callable surface.
Block syntax
Two forms — they differ only stylistically:
# Curly braces (single-expression):
[1, 2, 3].map { |n| n * 2 } # [2, 4, 6]
# do...end (multi-line):
[1, 2, 3].each do |n|
puts n
puts n * 2
end
The conventional discipline:
{ }for single-expression blocks and inline use.do...endfor multi-line blocks.
The precedence differs: { } binds tighter than do...end:
puts [1, 2].map { |n| n * 2 } # parses as: puts([1,2].map { ... })
puts [1, 2].map do |n| n * 2 end # parses as: puts([1,2]).map do ... end
# the do...end binds to puts!
The conventional defence: use { } for inline calls; do...end for multi-line where the result is assigned or returned.
yield
A method invokes its attached block with yield:
def each_three
yield 1
yield 2
yield 3
end
each_three { |n| puts n }
# 1
# 2
# 3
yield may pass any number of arguments (including zero or several):
def with_pair
yield 1, "one"
yield 2, "two"
end
with_pair do |num, word|
puts "#{num}: #{word}"
end
yield returns whatever the block returns:
def transform(value)
yield value
end
result = transform(5) { |n| n * 10 } # 50
block_given?
Methods may behave differently when no block is attached:
def fetch_or_compute
if block_given?
yield
else
default_value
end
end
fetch_or_compute # default_value
fetch_or_compute { compute_thing } # compute_thing
The pattern admits substantial flexibility — particularly in iterators that may return an Enumerator when no block is given:
class TodoList
def each
return to_enum(:each) unless block_given?
@items.each { |item| yield item }
end
end
list.each { |item| puts item } # iterates
list.each.lazy.select { ... } # returns Enumerator (chainable)
Capturing blocks with &block
The &block parameter captures the attached block as a Proc:
def with_logging(&block)
puts "before"
result = block.call
puts "after"
result
end
with_logging { puts "in block" }
# before
# in block
# after
The & is conventional only on the parameter declaration — within the method, block is a regular variable.
The captured form is required when:
- Storing the block — for later invocation.
- Passing the block to another method.
- Reflective inspection —
block.arity,block.lambda?.
class EventEmitter
def initialize
@listeners = []
end
def on(&block)
@listeners << block # store
end
def emit(event)
@listeners.each { |l| l.call(event) }
end
end
For passing to another method:
def proxy_method(&block)
underlying_method(&block) # & on the call: pass as block
end
Procs
A Proc is a first-class callable object. Several constructors:
p1 = Proc.new { |n| n * 2 }
p2 = proc { |n| n * 2 } # alias for Proc.new
p1.call(5) # 10
p1.(5) # 10 (alternative call syntax)
p1[5] # 10 (alternative call syntax)
Procs may be created from blocks via &:
def capture(&block)
block # the captured Proc
end
p = capture { |n| n * 2 }
p.call(5) # 10
Lambdas
A lambda is a Proc with stricter semantics. Two forms:
l1 = lambda { |n| n * 2 } # lambda keyword
l2 = ->(n) { n * 2 } # arrow form (since 1.9)
l1.call(5) # 10
l2.(5) # 10
l1.lambda? # true
The conventional contemporary form is -> for short lambdas; lambda { } for substantial bodies (rare).
Procs vs lambdas
The two differ in two principal ways:
Argument-count strictness
Lambdas check argument count strictly; procs are lenient:
l = ->(a, b) { a + b }
l.call(1) # ArgumentError: wrong number
p = proc { |a, b| a + b }
p.call(1) # 1 + nil → TypeError
# missing args become nil
p.call(1, 2, 3) # 3 (extra args ignored)
The lambda’s strictness admits substantial safety; the proc’s leniency admits convenience for variadic-style use.
return semantics
return in a lambda returns from the lambda; in a proc, it returns from the enclosing method:
def lambda_test
l = -> { return 10 }
l.call
20 # this runs
end
lambda_test # 20
def proc_test
p = proc { return 10 }
p.call
20 # this DOES NOT run
end
proc_test # 10
The proc’s return is a non-local return — it unwinds back to the defining method. If the defining method has already returned, calling the proc raises LocalJumpError.
The conventional discipline:
- Lambdas — for value-returning callables, conventional first-class functions.
- Procs — for “block-like” callables that may use non-local return.
- Blocks — for the conventional iteration and DSL patterns.
The & operator
The & is overloaded:
# In parameter list — capture block as Proc:
def f(&block); end
# In call — pass Proc as block:
m.method(&block)
[1, 2, 3].each(&block)
# Symbol to Proc:
[1, 2, 3].map(&:to_s) # ["1", "2", "3"]
The symbol-to-proc shorthand &:name is conventional:
[1, 2, 3].map(&:to_s) # equivalent to map { |x| x.to_s }
people.sort_by(&:age) # sort by age
strings.select(&:empty?) # select empty strings
The mechanism: &:name calls Symbol#to_proc, which produces proc { |x| x.send(:name) }.
Closures
Blocks, procs, and lambdas all close over their enclosing scope:
def make_counter
count = 0
-> {
count += 1
count
}
end
c1 = make_counter
c2 = make_counter
c1.call # 1
c1.call # 2
c2.call # 1 (independent)
Each call to make_counter produces a new closure with its own count.
Blocks as APIs
The conventional Ruby DSL pattern uses blocks substantially:
File.open("file.txt") do |f|
f.each_line { |line| puts line }
end # file closed automatically
[1, 2, 3].each_with_index do |item, i|
puts "[#{i}] #{item}"
end
10.times { |i| puts i }
The mechanism admits substantial resource management (open-close patterns), iteration, and DSL syntax.
instance_eval for DSLs
The instance_eval(&block) runs a block with self set to the receiver:
class Builder
def initialize(&block)
@lines = []
instance_eval(&block) if block
end
def add(line)
@lines << line
end
def to_s
@lines.join("\n")
end
end
b = Builder.new do
add "first" # no explicit receiver
add "second"
add "third"
end
puts b
# first
# second
# third
The mechanism admits substantial DSL syntax; treated in Metaprogramming.
Common patterns
Iterator method
class TodoList
def initialize
@items = []
end
def add(item)
@items << item
end
def each
@items.each { |item| yield item }
end
end
list = TodoList.new
list.add("buy milk")
list.add("write code")
list.each { |item| puts item }
Resource management
def with_timer
start = Time.now
result = yield
puts "elapsed: #{Time.now - start}"
result
end
result = with_timer { expensive_computation }
The pattern admits substantial open-close scoping for any resource.
Decorator/Around method
def with_retries(attempts = 3)
attempts.times do |i|
begin
return yield
rescue => e
raise if i == attempts - 1
sleep 2 ** i
end
end
end
result = with_retries { fetch_data }
Symbol-to-proc
[1, 2, 3].map(&:to_s) # ["1", "2", "3"]
strings.reject(&:empty?)
people.sort_by(&:age)
words.group_by(&:length)
Method-to-proc
class Calculator
def double(n)
n * 2
end
end
c = Calculator.new
[1, 2, 3].map(&c.method(:double)) # [2, 4, 6]
Block forwarding
def with_logging(&block)
log "before"
result = call_inner(&block) # forward block
log "after"
result
end
def call_inner
yield
end
Optional block
def process(items)
return enum_for(:process, items) unless block_given?
items.each do |item|
result = transform(item)
yield result
end
end
# Either way works:
process(items) { |r| puts r }
process(items).to_a # collects via Enumerator
Lambda for callbacks
on_click = ->(e) { puts "clicked at (#{e.x}, #{e.y})" }
on_keypress = ->(e) { puts "pressed #{e.key}" }
events.each do |event|
case event.type
when :click then on_click.call(event)
when :keypress then on_keypress.call(event)
end
end
Lambda factory
def adder(n)
->(x) { x + n }
end
add5 = adder(5)
add10 = adder(10)
add5.call(3) # 8
add10.call(3) # 13
Proc for stateful counter
def make_counter
count = 0
proc { count += 1 } # proc returns the new count
end
c = make_counter
c.call # 1
c.call # 2
Call syntax variations
l = ->(n) { n * 2 }
l.call(5) # explicit
l.(5) # parens form
l[5] # brackets form
l === 5 # case-equality form (returns 10)
The case-equality form is conventional in case/when — admits using lambdas as patterns:
positive = ->(n) { n > 0 }
case x
when positive then "positive"
end
instance_exec for DSL with arguments
class Config
def define_option(name, &block)
instance_exec(name, &block) # block runs with self = Config
# and admits the name argument
end
end
Curry
Lambdas admit currying:
add = ->(a, b, c) { a + b + c }
add_curried = add.curry
add5 = add_curried[5]
add5_10 = add5[10]
add5_10[20] # 35
# Or in one chain:
add.curry[1][2][3] # 6
A note on the conventional discipline
The contemporary Ruby block-and-proc advice:
- Use blocks for iteration and DSL patterns.
- Use
{ }for single-expression blocks;do...endfor multi-line. - Use
yieldin methods that take a block. - Use
block_given?to support optional blocks. - Use
&blockparameter to capture the block as aProc. - Use
&in calls to pass aProcas a block. - Use lambdas (
->) for first-class function values. - Use procs for block-like callables (rare in modern code).
- Use
&:namefor symbol-to-proc shortcuts. - Use
instance_eval/instance_execfor DSLs. - Use
enum_for(:method, args)to return an Enumerator from a block-receiving method. - Use lambdas over procs for value-returning callables.
The combination — implicit blocks via do...end/{ }, yield for invocation, &block for capture, procs and lambdas as first-class objects, the symbol-to-proc shorthand, the closures over enclosing scope — is the substance of Ruby’s callable surface. The discipline produces concise, expressive code with substantial flexibility for callbacks, iteration, and DSL design.