Documentation/Buki/Ruby/ skills /ruby-blocks-procs-lambdas

📖 ruby-blocks-procs-lambdas

Use when working with Ruby blocks, procs, lambdas, and functional programming patterns including closures and higher-order functions.



Overview

Master Ruby's functional programming features with blocks, procs, and lambdas. These are fundamental to Ruby's expressive and elegant style.

Blocks

Basic Block Syntax

# Block with do...end (multi-line)
[1, 2, 3].each do |num|
  puts num * 2
end

# Block with {...} (single line)
[1, 2, 3].each { |num| puts num * 2 }

Yielding to Blocks

def repeat(times)
  times.times do
    yield  # Execute the block
  end
end

repeat(3) { puts "Hello" }

# With block parameters
def greet
  yield("World")
end

greet { |name| puts "Hello, #{name}!" }

Block Arguments

def process_data(data)
  result = yield(data)
  puts "Result: #{result}"
end

process_data(10) { |x| x * 2 }  # Result: 20

Checking for Blocks

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

optional_block { puts "Block executed" }
optional_block

Block Local Variables

x = 10

[1, 2, 3].each do |num; local_var|
  local_var = num * 2  # local_var only exists in block
  puts local_var
end

puts x  # 10 (unchanged)

Procs

Creating Procs

# Using Proc.new
my_proc = Proc.new { |x| x * 2 }
puts my_proc.call(5)  # 10

# Using proc method (deprecated in some versions)
my_proc = proc { |x| x * 2 }

# Using -> (stabby lambda syntax for Proc)
my_proc = ->(x) { x * 2 }

Proc Characteristics

# Procs don't care about argument count
flexible_proc = Proc.new { |x, y| "x: #{x}, y: #{y}" }
puts flexible_proc.call(1)     # x: 1, y:
puts flexible_proc.call(1, 2, 3)  # x: 1, y: 2 (ignores extra)

# Procs return from the enclosing method
def proc_return
  my_proc = Proc.new { return "from proc" }
  my_proc.call
  "after proc"  # Never reached
end

puts proc_return  # "from proc"

Passing Procs as Arguments

def execute_proc(my_proc)
  my_proc.call
end

greeting = Proc.new { puts "Hello from proc!" }
execute_proc(greeting)

Converting Blocks to Procs

def method_with_proc(&block)
  block.call
end

method_with_proc { puts "Block converted to proc" }

Lambdas

Creating Lambdas

# Using lambda keyword
my_lambda = lambda { |x| x * 2 }

# Using -> (stabby lambda)
my_lambda = ->(x) { x * 2 }

# Multi-line stabby lambda
my_lambda = ->(x) do
  result = x * 2
  result + 1
end

puts my_lambda.call(5)  # 11

Lambda Characteristics

# Lambdas enforce argument count
strict_lambda = ->(x, y) { x + y }
# strict_lambda.call(1)     # ArgumentError
strict_lambda.call(1, 2)    # Works

# Lambdas return to the caller
def lambda_return
  my_lambda = -> { return "from lambda" }
  my_lambda.call
  "after lambda"  # This IS reached
end

puts lambda_return  # "after lambda"

Lambda with Multiple Arguments

add = ->(x, y) { x + y }
multiply = ->(x, y, z) { x * y * z }

puts add.call(3, 4)         # 7
puts multiply.call(2, 3, 4) # 24

# Default arguments
greet = ->(name = "World") { "Hello, #{name}!" }
puts greet.call           # "Hello, World!"
puts greet.call("Ruby")   # "Hello, Ruby!"

Proc vs Lambda

# Argument handling
my_proc = Proc.new { |x, y| puts "x: #{x}, y: #{y}" }
my_lambda = ->(x, y) { puts "x: #{x}, y: #{y}" }

my_proc.call(1)     # Works: x: 1, y:
# my_lambda.call(1) # ArgumentError

# Return behavior
def test_return
  proc_test = Proc.new { return "proc return" }
  lambda_test = -> { return "lambda return" }

  proc_test.call   # Returns from method
  "end"            # Never reached
end

def test_lambda
  lambda_test = -> { return "lambda return" }
  lambda_test.call # Returns from lambda
  "end"            # This IS reached
end

# Check if it's a lambda
my_proc = Proc.new { }
my_lambda = -> { }

puts my_proc.lambda?   # false
puts my_lambda.lambda? # true

Closures

def multiplier(factor)
  ->(x) { x * factor }
end

times_two = multiplier(2)
times_three = multiplier(3)

puts times_two.call(5)    # 10
puts times_three.call(5)  # 15

# Closures capture variables
def counter
  count = 0

  increment = -> { count += 1 }
  decrement = -> { count -= 1 }
  value = -> { count }

  [increment, decrement, value]
end

inc, dec, val = counter
inc.call
inc.call
puts val.call  # 2
dec.call
puts val.call  # 1

Method Objects

class Calculator
  def add(x, y)
    x + y
  end
end

calc = Calculator.new
add_method = calc.method(:add)
puts add_method.call(3, 4)  # 7

# Converting methods to procs
add_proc = calc.method(:add).to_proc
puts add_proc.call(5, 6)  # 11

Symbol to Proc

# & converts symbol to proc
numbers = [1, 2, 3, 4, 5]

# These are equivalent:
numbers.map { |n| n.to_s }
numbers.map(&:to_s)

# Works with any method
["hello", "world"].map(&:upcase)  # ["HELLO", "WORLD"]
[1, 2, 3].select(&:even?)         # [2]

Higher-Order Functions

def compose(f, g)
  ->(x) { f.call(g.call(x)) }
end

double = ->(x) { x * 2 }
square = ->(x) { x * x }

double_then_square = compose(square, double)
puts double_then_square.call(3)  # 36 (3 * 2 = 6, 6 * 6 = 36)

Currying

# Manual currying
add = ->(x) { ->(y) { x + y } }
add_five = add.call(5)
puts add_five.call(3)  # 8

# Built-in currying
multiply = ->(x, y, z) { x * y * z }
curried = multiply.curry
times_two = curried.call(2)
times_two_three = times_two.call(3)
puts times_two_three.call(4)  # 24

# Partial application
puts curried.call(2, 3).call(4)  # 24

Practical Patterns

Lazy Evaluation

def lazy_value
  puts "Computing expensive value..."
  42
end

# Wrap in lambda for lazy evaluation
lazy = -> { lazy_value }

puts "Before call"
result = lazy.call  # Only computed here
puts result

Callback Pattern

class Button
  def initialize
    @on_click = []
  end

  def on_click(&block)
    @on_click << block
  end

  def click
    @on_click.each(&:call)
  end
end

button = Button.new
button.on_click { puts "Button clicked!" }
button.on_click { puts "Another handler" }
button.click

Strategy Pattern

class Sorter
  def initialize(strategy)
    @strategy = strategy
  end

  def sort(array)
    @strategy.call(array)
  end
end

ascending = ->(arr) { arr.sort }
descending = ->(arr) { arr.sort.reverse }

sorter = Sorter.new(ascending)
puts sorter.sort([3, 1, 2])  # [1, 2, 3]

sorter = Sorter.new(descending)
puts sorter.sort([3, 1, 2])  # [3, 2, 1]

Memoization

def memoize(&block)
  cache = {}
  ->(arg) do
    cache[arg] ||= block.call(arg)
  end
end

expensive_operation = memoize do |n|
  puts "Computing for #{n}..."
  n * n
end

puts expensive_operation.call(5)  # Computing for 5... 25
puts expensive_operation.call(5)  # 25 (cached)

Best Practices

  1. Use blocks for simple iteration and single-use closures
  2. Use lambdas for strict argument checking and returnable closures
  3. Use procs for flexible argument handling (rare cases)
  4. Prefer -> syntax for lambdas (more concise)
  5. Use &:symbol for simple method calls on collections
  6. Leverage closures for encapsulation and data privacy
  7. Use block_given? before yielding to optional blocks

Anti-Patterns

❌ Don't use Proc.new for strict behavior - use lambda instead ❌ Don't ignore return behavior - understand proc vs lambda differences ❌ Don't overuse closures - can lead to memory leaks if not careful ❌ Don't create deeply nested lambdas - hard to read and debug ❌ Don't forget to handle missing blocks - check with block_given?

Related Skills

  • ruby-oop - For understanding method context
  • ruby-metaprogramming - For dynamic block/proc usage
  • ruby-standard-library - For Enumerable methods using blocks