Ruby Blocks Explained: Understanding {|x| x * x} Syntax

Ruby Blocks Explained: Understanding {|x| x * x} Syntax

Ruby blocks are chunks of code you pass to methods. They're everywhere:

[1, 2, 3].map { |n| n * 2 }  # [2, 4, 6]

The { |n| n * 2 } part is a block. It receives each element (n) and returns n * 2. The map method calls the block for each element, collecting the results.

Block Syntax

Blocks come in two forms:

Curly braces (single-line):

{ |param| expression }

do/end (multi-line):

do |param|
  expression
end

By convention, use curly braces for one-liners and do/end for multi-line blocks:

# Single line
[1, 2, 3].each { |n| puts n }

# Multi-line
[1, 2, 3].each do |n|
  squared = n * n
  puts squared
end

How Blocks Work

Methods receive blocks implicitly. Inside a method, yield calls the block:

def my_method
  puts "Before block"
  yield
  puts "After block"
end

my_method { puts "Inside block" }

# Output:
# Before block
# Inside block
# After block

You can pass arguments to blocks through yield:

def twice
  yield(1)
  yield(2)
end

twice { |n| puts n * 2 }

# Output:
# 2
# 4

Block Parameters

Blocks can take multiple parameters:

{ "a" => 1, "b" => 2 }.each { |key, value| puts "#{key}: #{value}" }

# Output:
# a: 1
# b: 2

The number of parameters depends on what the method yields.

Implicit Return

Blocks return the value of the last expression:

[1, 2, 3].map { |n| n * 2 }  # [2, 4, 6]
# The block returns n * 2 implicitly

Explicit return exits the enclosing method, not just the block:

def example
  [1, 2, 3].each do |n|
    return "Found 2" if n == 2
  end
  "Not found"
end

example  # "Found 2"

The return exits example, not just the block. To exit only the block, use next:

def example
  [1, 2, 3].each do |n|
    next if n == 2  # Skip to next iteration
    puts n
  end
end

example
# Output:
# 1
# 3

Common Iterator Methods

each - iterates without transforming:

[1, 2, 3].each { |n| puts n }

map - transforms each element:

[1, 2, 3].map { |n| n * 2 }  # [2, 4, 6]

select - filters by condition:

[1, 2, 3, 4].select { |n| n.even? }  # [2, 4]

reject - inverse of select:

[1, 2, 3, 4].reject { |n| n.even? }  # [1, 3]

reduce - accumulates a result:

[1, 2, 3].reduce(0) { |sum, n| sum + n }  # 6

Blocks vs Procs vs Lambdas

Ruby has three ways to create reusable code chunks:

Blocks - not objects, passed implicitly:

[1, 2, 3].map { |n| n * 2 }

Procs - objects, stored in variables:

doubler = Proc.new { |n| n * 2 }
[1, 2, 3].map(&doubler)  # [2, 4, 6]

Lambdas - procs with stricter argument checking:

doubler = ->(n) { n * 2 }
[1, 2, 3].map(&doubler)  # [2, 4, 6]

Key differences:

  • Return behavior: return in a lambda exits the lambda. return in a proc exits the enclosing method.
  • Argument checking: Lambdas raise errors for wrong argument counts. Procs ignore extra arguments or assign nil to missing ones.
proc = Proc.new { |a, b| puts "#{a}, #{b}" }
proc.call(1)  # "1, " (b is nil)

lam = ->(a, b) { puts "#{a}, #{b}" }
lam.call(1)  # ArgumentError: wrong number of arguments

Converting Blocks to Procs

Methods can capture blocks as procs using &:

def my_method(&block)
  block.call
end

my_method { puts "Hello" }

This converts the block to a proc, making it an object you can store or pass around.

Yielding with block_given?

Check if a block was passed before yielding:

def my_method
  if block_given?
    yield
  else
    puts "No block provided"
  end
end

my_method  # "No block provided"
my_method { puts "Block!" }  # "Block!"

Practical Examples

Custom iteration:

def times_three
  3.times { |i| yield(i) }
end

times_three { |n| puts n }
# Output: 0, 1, 2

Resource management:

def with_file(path)
  file = File.open(path)
  yield(file)
ensure
  file.close if file
end

with_file("data.txt") do |f|
  puts f.read
end

The file is automatically closed, even if the block raises an error.

Configuration DSL:

class Config
  def initialize
    yield(self) if block_given?
  end
  
  attr_accessor :name, :value
end

config = Config.new do |c|
  c.name = "app"
  c.value = 42
end

Stabby Lambda Syntax

Ruby 1.9 introduced -> syntax for lambdas:

# Traditional
add = lambda { |a, b| a + b }

# Stabby lambda
add = ->(a, b) { a + b }

Parameters go in parentheses before the block. Multi-line works too:

greet = ->(name) do
  puts "Hello, #{name}!"
end

greet.call("Alice")

When to Use Each

Blocks - for passing behavior to methods:

[1, 2, 3].each { |n| puts n }

Procs - when you need to store code for later or pass it to multiple methods:

validator = Proc.new { |n| n > 0 }
positives = numbers.select(&validator)
all_positive = numbers.all?(&validator)

Lambdas - when you want strict argument checking and lambda-style returns:

operation = ->(a, b) { a + b }
result = operation.call(2, 3)

Performance

Blocks are slightly faster than procs/lambdas because they're not objects. For tight loops, this might matter:

# Faster
1000000.times { |i| i * 2 }

# Slightly slower
operation = ->(i) { i * 2 }
1000000.times(&operation)

But the difference is negligible in most code.

Further Reading

The Ruby documentation on Proc covers procs and lambdas in detail.

Paolo Perrotta's Metaprogramming Ruby has excellent chapters on blocks, closures, and the different callable objects in Ruby.

The Ruby Style Guide's section on blocks provides style conventions.

Blocks are central to Ruby's expressiveness.

Wear the code

Product mockup

{|x| x _ x} Developer T-Shirt (Ruby Edition — Dark Mode)

£25.00

View product
Product mockup

{|x| x _ x} Developer T-Shirt (Ruby Edition — Light Mode)

£25.00

View product

0 comments

Leave a comment

Please note, comments need to be approved before they are published.