Ruby Blocks, Procs & Lambdas

Blocks

Block syntax — do/end vs braces
# Brace style (single-line convention)
[80, 443, 8080].each { |port| puts port }

# do/end style (multi-line convention)
[80, 443, 8080].each do |port|
  result = scan(port)
  puts "#{port}: #{result}"
end

# Block with multiple parameters
{ web: 443, ssh: 22 }.each do |service, port|
  puts "#{service} => #{port}"
end
Yielding to blocks
# Method that yields
def with_retry(max = 3)
  attempt = 0
  begin
    attempt += 1
    yield attempt
  rescue StandardError => e
    retry if attempt < max
    raise e
  end
end

with_retry(5) do |n|
  puts "Attempt #{n}"
  connect_to_server
end

# Check if block given
def log(message)
  output = "[#{Time.now}] #{message}"
  if block_given?
    yield output
  else
    puts output
  end
end

log("connected") { |msg| File.write("app.log", msg, mode: "a") }

Procs

Proc creation and usage
# Proc.new
formatter = Proc.new { |msg| "[INFO] #{msg}" }
puts formatter.call("server started")
puts formatter.("also works")
puts formatter["bracket syntax"]

# proc method (lowercase)
square = proc { |x| x ** 2 }
[1, 2, 3].map(&square)                # => [1, 4, 9]

# Proc characteristics:
# - Lenient arity (extra args ignored, missing args = nil)
# - return exits enclosing method
p = proc { |a, b| a }
p.call(1)         # => 1 (b is nil, no error)
p.call(1, 2, 3)   # => 1 (extra arg ignored)
Converting methods to procs
# Symbol#to_proc with &
names = ["vault", "ISE", "bind"]
puts names.map(&:upcase)               # => ["VAULT", "ISE", "BIND"]
puts names.select(&:ascii_only?)

# Method reference
method_ref = method(:puts)
[1, 2, 3].each(&method_ref)

Lambdas

Lambda creation and behavior
# Stabby lambda
greet = ->(name) { "Hello, #{name}" }
puts greet.call("Evan")
puts greet.("Evan")

# Multi-line stabby lambda
process = ->(host, port) do
  puts "Connecting to #{host}:#{port}"
  TCPSocket.new(host, port)
end

# Lambda vs Proc differences:
# 1. Lambdas enforce argument count
lam = ->(a, b) { a + b }
lam.call(1, 2)         # => 3
# lam.call(1)          # ArgumentError

# 2. Lambda return goes to caller; Proc return exits enclosing method
def test_lambda
  lam = -> { return "from lambda" }
  lam.call
  "after lambda"       # this executes
end

def test_proc
  prc = proc { return "from proc" }
  prc.call
  "after proc"         # never reached
end

# 3. Check type
lam = -> {}
lam.lambda?            # => true

Closures

Blocks, procs, and lambdas are all closures
# They capture the enclosing scope
def make_counter(start = 0)
  count = start
  incrementer = -> { count += 1; count }
  getter      = -> { count }
  [incrementer, getter]
end

inc, get = make_counter(10)
inc.call    # => 11
inc.call    # => 12
get.call    # => 12

# Practical: config builder
def configure(defaults = {})
  config = defaults.dup
  yield config
  config.freeze
end

settings = configure(retries: 3) do |c|
  c[:timeout] = 30
  c[:host] = "10.50.1.20"
end