Polyglot
Languages Ruby blocks
Ruby § blocks

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...end for 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 inspectionblock.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...end for multi-line.
  • Use yield in methods that take a block.
  • Use block_given? to support optional blocks.
  • Use &block parameter to capture the block as a Proc.
  • Use & in calls to pass a Proc as a block.
  • Use lambdas (->) for first-class function values.
  • Use procs for block-like callables (rare in modern code).
  • Use &:name for symbol-to-proc shortcuts.
  • Use instance_eval/instance_exec for 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.