Skip to content

Methods and Blocks | Ruby - Wyatt's Notes

## Simple method definition
def greet
puts "Hello, World!"
end
greet # => prints "Hello, World!"
## Method with parameters
def greet(name)
puts "Hello, #{name}!"
end
greet("Alice") # => "Hello, Alice!"
greet "Alice" # parentheses are optional for method calls
# Method with return value (last expression is returned)
def add(a, b)
result = a + b
result # this is the return value
end
# Implicit return (idiomatic)
def add(a, b)
a + b
end
# Explicit return
def add(a, b)
return a + b
end
# Return vs last expression
def example
return 42 if some_condition
"default value"
end
# Method with multiple expressions
def calculate_statistics(numbers)
sum = numbers.sum
mean = sum.to_f / numbers.size
sorted = numbers.sort
median = numbers.size.odd? ? sorted[sorted.size / 2] : (sorted[sorted.size / 2 - 1] + sorted[sorted.size / 2]) / 2.0
{ sum: sum, mean: mean, median: median, size: numbers.size }
end
# Default parameter values
def greet(name = "World")
"Hello, #{name}!"
end
greet # => "Hello, World!"
greet("Alice") # => "Hello, Alice!"
# Multiple defaults
def connect(host = "localhost", port = 5432, timeout = 30)
{ host: host, port: port, timeout: timeout }
end
connect # => { host: "localhost", port: 5432, timeout: 30 }
connect("db.example.com") # => { host: "db.example.com", port: 5432, timeout: 30 }
connect("db.example.com", 3306, 60)
# Defaults can reference earlier parameters
def create_range(start_val = 0, end_val = start_val + 10)
(start_val..end_val)
end
# Be careful with mutable defaults
# BAD -- same array is reused
def add_item(items = [])
items << "new"
items
end
# GOOD -- use nil and create inside
def add_item(items = nil)
items ||= []
items << "new"
items
end
# Basic keyword arguments
def configure(host:, port:, timeout:)
puts "#{host}:#{port} (#{timeout}s)"
end
configure(host: "localhost", port: 8080, timeout: 30)
# Keyword arguments with defaults
def configure(host: "localhost", port: 8080, timeout: 30)
puts "#{host}:#{port} (#{timeout}s)"
end
configure # => "localhost:8080 (30s)"
configure(host: "example.com", port: 3000) # => "example.com:3000 (30s)"
# Mixing positional and keyword arguments
def create_user(name, email, admin: false, active: true)
{ name: name, email: email, admin: admin, active: active }
end
create_user("Alice", "alice@example.com")
create_user("Bob", "bob@example.com", admin: true)
# Double splat for accepting arbitrary keyword arguments
def accept_any(**kwargs)
kwargs
end
accept_any(a: 1, b: 2, c: 3)
# => { a: 1, b: 2, c: 3 }
# Required keyword arguments (no default)
def required_kw(name:, email:)
"#{name} <#{email}>"
end
required_kw(name: "Alice", email: "a@b.com")
# Keyword splat to pass through
def wrapper(host:, port:, **other_options)
actual_connect(host: host, port: port, **other_options)
end
# Splat operator * collects remaining positional arguments into an array
def sum(*numbers)
numbers.reduce(0, :+)
end
sum(1, 2, 3) # => 6
sum(1, 2, 3, 4, 5) # => 15
# Splat with required arguments
def log(level, *messages)
messages.each { |msg| puts "[#{level}] #{msg}" }
end
log("INFO", "Server started", "Listening on port 8080")
# Splat to expand an array into arguments
def add(a, b, c)
a + b + c
end
numbers = [1, 2, 3]
add(*numbers) # => 6
# Splat in the middle
def between(first, *middle, last)
puts "First: #{first}, Middle: #{middle}, Last: #{last}"
end
between(1, 2, 3, 4, 5)
# => First: 1, Middle: [2, 3, 4], Last: 5
# Double splat ** for keyword arguments
def options(**opts)
opts.each { |k, v| puts "#{k}: #{v}" }
end
options(color: "red", size: "large")
# Splat and double splat together
def flexible(*args, **kwargs)
puts "Args: #{args.inspect}"
puts "Kwargs: #{kwargs.inspect}"
end
flexible(1, 2, 3, a: "x", b: "y")
# Args: [1, 2, 3]
# Kwargs: {:a=>"x", :b=>"y"}
# Method aliasing
class String
alias :sentence_case :capitalize
end
"hello world".sentence_case # => "Hello world"
# Override a method while preserving the original
class Array
def sum
reduce(0) { |acc, elem| acc + (elem.is_a?(Numeric) ? elem : 0) }
end
end
# Prevent overriding
class String
freeze
end
# Last expression is the return value
def multiply(a, b)
a * b
end
multiply(3, 4) # => 12
# Explicit return exits immediately
def first_positive(numbers)
numbers.each do |n|
return n if n > 0
end
nil # default return if loop completes without finding
end
# Return multiple values (actually returns an array)
def min_max(arr)
[arr.min, arr.max]
end
result = min_max([3, 1, 4, 1, 5])
result # => [1, 5]
min_val, max_val = min_max([3, 1, 4, 1, 5])
puts min_val # => 1
puts max_val # => 5
# Return nothing (returns nil)
def log(message)
puts message
# implicit return of puts result (which is nil)
end
result = log("test")
result.nil? # => true
# Return from a block
# return in a block returns from the enclosing METHOD, not the block
def find_even(array)
array.each do |n|
return n if n.even? # returns from find_even, not from the block
end
nil
end
# next returns from the block only
def find_even_with_next(array)
array.each do |n|
next if n.odd?
return n # only reached for even numbers
end
nil
end

Blocks are anonymous chunks of code that can be passed to methods. They are one of Ruby”s most powerful features.

# Block with do..end (multi-line convention)
[1, 2, 3].each do |n|
puts n
end
# Block with {} (single-line convention)
[1, 2, 3].each { |n| puts n }
# Block with multiple parameters
{ a: 1, b: 2 }.each do |key, value|
puts "#{key}: #{value}"
end
# Block without parameters
3.times { puts "hello" }
# Implicit block variable: use & to capture the block
# Numbered parameters (Ruby 2.7+)
[1, 2, 3].map { _1 * 2 } # => [2, 4, 6]
[1, 2, 3].zip([4, 5, 6]).map { "#{_1}-#{_2}" } # => ["1-4", "2-5", "3-6"]

The yield keyword calls the block passed to the method:

# Method that yields to a block
def greet
puts "Before yield"
yield
puts "After yield"
end
greet { puts "Inside block" }
# Output:
# Before yield
# Inside block
# After yield
# Yield with arguments
def each_item(items)
items.each { |item| yield(item) }
end
each_item([1, 2, 3]) { |n| puts n * 10 }
# => 10, 20, 30
# Yield with multiple arguments
def pairs
yield(1, "a")
yield(2, "b")
yield(3, "c")
end
pairs { |number, letter| puts "#{number}: #{letter}" }
# Check if a block was given
def maybe_yield
if block_given?
yield
else
puts "No block provided"
end
end
maybe_yield { puts "Block here" } # => "Block here"
maybe_yield # => "No block provided"
# Custom iterator using yield
def my_each(array)
index = 0
while index < array.length
yield(array[index])
index += 1
end
end
my_each([10, 20, 30]) { |n| puts n }
# Custom map using yield
def my_map(array)
result = []
array.each { |element| result << yield(element) }
result
end
my_map([1, 2, 3]) { |n| n ** 2 } # => [1, 4, 9]
# Custom select using yield
def my_select(array)
result = []
array.each { |element| result << element if yield(element) }
result
end
my_select([1, 2, 3, 4, 5]) { |n| n.even? } # => [2, 4]
# & converts a block to a Proc and vice versa
def run_twice(&block)
block.call
block.call
end
run_twice { puts "hello" }
# => "hello" (printed twice)
# Explicit block parameter
def apply_to_each(array, &block)
array.each { |element| block.call(element) }
end
apply_to_each([1, 2, 3]) { |n| puts n * 2 }

Procs are stored blocks — you can save a block, pass it around, and call it later:

# Creating a Proc
greeter = Proc.new { |name| puts "Hello, #{name}!" }
greeter.call("Alice") # => "Hello, Alice!"
greeter.("Alice") # shorthand
greeter["Alice"] # also works
# Proc.new with a block
my_proc = Proc.new { puts "I am a proc" }
# proc method
another_proc = proc { |n| n * 2 }
# Passing a proc as a block with &
multiplier = proc { |n| n * 2 }
[1, 2, 3].map(&multiplier) # => [2, 4, 6]
# Procs have lenient arity
flexible = Proc.new { |a, b| puts "#{a}, #{b}" }
flexible.call(1, 2) # => "1, 2"
flexible.call(1) # => "1, " (missing arg filled with nil)
flexible.call(1, 2, 3) # => "1, 2" (extra arg ignored)
# Procs return from the enclosing method
def return_from_proc
p = Proc.new { return 42 }
p.call
puts "This line never executes"
end
return_from_proc # => 42 (returns from the method)
# Procs are closures -- they capture their surrounding environment
def counter
count = 0
increment = Proc.new { count += 1 }
increment
end
c = counter
c.call # => 1
c.call # => 2
c.call # => 3

Lambdas are a special type of Proc with strict argument checking and return semantics:

# Creating a lambda
greet = -> (name) { puts "Hello, #{name}!" }
greet.call("Alice") # => "Hello, Alice!"
# Shorthand lambda
square = ->(n) { n ** 2 }
square.call(5) # => 25
# Multi-line lambda
calculate = ->(a, b) do
sum = a + b
product = a * b
{ sum: sum, product: product }
end
calculate.call(3, 4) # => { sum: 7, product: 12 }
# Lambda vs Proc: strict arity
strict = ->(a, b) { a + b }
strict.call(1, 2) # => 3
strict.call(1) # => ArgumentError (wrong number of arguments)
strict.call(1, 2, 3) # => ArgumentError
# Lambda vs Proc: return semantics
def return_from_lambda
l = -> { return 42 }
l.call
puts "This line DOES execute"
end
return_from_lambda # => prints "This line DOES execute", returns nil from the method
FeatureProcLambda
CreationProc.new {} or proc {}-> {} or lambda {}
Arity checkLenient (fills with nil)Strict (raises ArgumentError)
ReturnReturns from enclosing methodReturns from lambda only
lambda?falsetrue
# Checking the type
p = Proc.new {}
l = lambda {}
p.lambda? # => false
l.lambda? # => true
# Both are Proc objects
p.class # => Proc
l.class # => Proc
# Practical example
class Event
def initialize
@handlers = []
end
def on(&handler)
@handlers << handler
end
def trigger(*args)
@handlers.each { |h| h.call(*args) }
end
end
button = Event.new
button.on { puts "Clicked!" }
button.on { |x, y| puts "Clicked at (#{x}, #{y})" }
button.trigger(100, 200)

Ruby methods can be converted into objects that respond to .call:

class Calculator
def add(a, b)
a + b
end
def multiply(a, b)
a * b
end
end
calc = Calculator.new
# Convert method to Method object
add_method = calc.method(:add)
add_method.call(3, 4) # => 7
add_method.call(10, 20) # => 30
add_method.arity # => 2
# UnboundMethod -- method without a receiver
unbound = Calculator.instance_method(:add)
bound = unbound.bind(calc)
bound.call(3, 4) # => 7
# Bind to a different object
calc2 = Calculator.new
bound2 = unbound.bind(calc2)
bound2.call(5, 6) # => 11
# Method introspection
add_method.name # => :add
add_method.owner # => Calculator
add_method.receiver # => #<Calculator:...>
add_method.parameters # => [[:req, :a], [:req, :b]]
add_method.source_location # => ["/path/to/file.rb", 2]
# Convert Method to Proc
add_proc = add_method.to_proc
[1, 2, 3].map(&add_method) # Would need arity 1
# Useful pattern: passing methods as blocks
["hello", "world"].map(&:upcase) # => ["HELLO", "WORLD"]
[1, 2, 3].map(&:to_s) # => ["1", "2", "3"]
# &:method_name converts symbol to Proc
# Equivalent to:
["hello", "world"].map { |s| s.upcase }

define_method dynamically creates methods at runtime:

class Person
# Dynamic method definitions
[:name, :email, :phone].each do |field|
define_method(field) { instance_variable_get("@#{field}") }
define_method("#{field}=") { |value| instance_variable_set("@#{field}", value) }
end
define_method(:greet) do |greeting = "Hello"|
"#{greeting}, #{@name}!"
end
end
p = Person.new
p.name = "Alice"
p.name # => "Alice"
p.greet # => "Hello, Alice!"
p.greet("Hi") # => "Hi, Alice!"
# define_method with a Proc
class Array
define_method(:second) { self[1] }
define_method(:third) { self[2] }
end
[10, 20, 30].second # => 20
[10, 20, 30].third # => 30
# define_method with lambda
class Formatter
define_method(:format_price, ->(amount) do
"$#{'%.2f' % amount}"
end)
end
Formatter.new.format_price(42.5) # => "$42.50"
# Dynamic attribute methods
class Model
def self.attributes(*attrs)
attrs.each do |attr|
define_method(attr) { @attributes[attr.to_s] }
define_method("#{attr}=") { |val| @attributes[attr.to_s] = val }
define_method("#{attr}?") { !@attributes[attr.to_s].nil? }
end
end
def initialize(attrs = {})
@attributes = attrs
end
end
class User < Model
attributes :name, :email, :age
end
u = User.new(name: "Alice", email: "a@b.com")
u.name? # => true
u.email # => "a@b.com"

When Ruby cannot find a method on an object, it calls method_missing before raising NoMethodError:

class DynamicAccess
def initialize(data = {})
@data = data
end
def method_missing(name, *args)
key = name.to_s
if key.end_with?("=")
@data[key.chomp("=")] = args.first
elsif @data.key?(key)
@data[key]
else
super
end
end
def respond_to_missing?(name, include_private = false)
key = name.to_s
key.end_with?("=") || @data.key?(key) || super
end
end
obj = DynamicAccess.new(name: "Alice", age: 30)
obj.name # => "Alice"
obj.age # => 30
obj.city = "NYC"
obj.city # => "NYC"
obj.missing # => NoMethodError
# respond_to? works because we defined respond_to_missing?
obj.respond_to?(:name) # => true
obj.respond_to?(:missing) # => false
# Practical use: dynamic finders (ActiveRecord pattern)
class FakeActiveRecord
def initialize
@records = [
{ id: 1, name: "Alice", role: "admin" },
{ id: 2, name: "Bob", role: "user" },
{ id: 3, name: "Charlie", role: "admin" },
]
end
def method_missing(name, *args)
match = name.to_s.match(/^find_by_(.+)$/)
if match
field = match[1]
@records.find { |r| r[field.to_sym] == args.first }
else
super
end
end
def respond_to_missing?(name, include_private = false)
name.to_s.match(/^find_by_.+$/) || super
end
end
ar = FakeActiveRecord.new
ar.find_by_name("Bob") # => { id: 2, name: "Bob", role: "user" }
ar.find_by_role("admin") # => { id: 1, name: "Alice", role: "admin" }
# Always define respond_to_missing? with method_missing
class SafeDynamic
def method_missing(name, *args, &block)
return super unless name.to_s.start_with?("dynamic_")
field = name.to_s.sub("dynamic_", "")
@store[field] = args.first
end
def respond_to_missing?(name, include_private = false)
name.to_s.start_with?("dynamic_") || super
end
end
# Avoiding method_missing pitfalls
# 1. Always call super if you don't handle the method
# 2. Always define respond_to_missing?
# 3. Be aware of performance -- method_missing is slower than real methods
# 4. Debugging is harder -- methods are invisible to introspection
# Blocks are closures -- they capture variables from enclosing scope
x = 10
y = 20
[1, 2, 3].each do |n|
puts "#{n}, #{x}" # x is accessible
end
# Blocks can modify enclosing variables
counter = 0
[1, 2, 3].each do |n|
counter += n
end
puts counter # => 6
# Block-local variables (Ruby 1.9+)
[1, 2, 3].each do |n; local_n|
local_n = n * 10 # does not affect n outside
puts local_n
end
# Thread-safety with blocks
shared = []
mutex = Mutex.new
[1, 2, 3].each do |n|
mutex.synchronize do
shared << n * 2
end
end
class HTMLBuilder
def initialize
@buffer = ""
end
def build
yield self
@buffer
end
def tag(name, attrs = {})
@buffer << "<#{name}"
attrs.each { |k, v| @buffer << " #{k}=\"#{v}\"" }
@buffer << ">"
@buffer << yield if block_given?
@buffer << "</#{name}>"
end
def text(content)
@buffer << content
end
def to_s
@buffer
end
end
html = HTMLBuilder.new.build do |b|
b.tag(:div, class: "container") do
b.tag(:h1) { b.text("Title") }
b.tag(:p) { b.text("Content") }
end
end
def time_it
start = Time.now
result = yield
elapsed = Time.now - start
puts "Took #{elapsed.round(4)}s"
result
end
time_it { sleep(0.1); "done" }
# => "Took 0.1001s", returns "done"
def retry_on(error_class, max_attempts: 3)
attempts = 0
begin
attempts += 1
yield
rescue error_class => e
raise if attempts >= max_attempts
sleep(0.1 * attempts)
retry
end
end
result = retry_on(TimeoutError) { fetch_data }
class Cache
def initialize
@store = {}
end
def fetch(key)
return @store[key] if @store.key?(key)
@store[key] = yield
end
end
cache = Cache.new
cache.fetch("expensive") { compute_expensive_result }
cache.fetch("expensive") { compute_expensive_result } # returns cached value
flowchart TD
    A[1_Methods And Blocks] --> B[Key Concepts]
    A --> C[Core Principles]
    A --> D[Practical Applications]
    B --> E[Fundamental definitions]
    C --> F[Design patterns]
    D --> G[Real-world usage]

Blocks in Ruby are like closures, which is a fancy way of saying they remember the context where they were created. When you define a block inside a method, it captures the local variables from that method. Even if the method finishes, the block still has access to those variables. This is like a photograph that captures not just the subject but the entire background.

Procs and lambdas are like different flavors of blocks. A Proc is a casual block that does not care about argument counts. A lambda is a strict block that enforces its argument rules. Using a lambda is like calling a function with a signature; using a Proc is like calling a function that accepts whatever you give it.

Example 1: Building a Configuration DSL with Blocks

Section titled “Example 1: Building a Configuration DSL with Blocks”

Problem: Create a DSL that allows users to configure an application using a clean block-based syntax.

class Config
attr_reader :settings
def initialize
@settings = {}
end
def configure
yield self
@settings
end
def setting(key, value = nil)
if block_given?
@settings[key] = yield
else
@settings[key] = value
end
end
def group(name)
@settings[name] ||= {}
yield @settings[name] if block_given?
self
end
end
config = Config.new
config.configure do |c|
c.setting :host, "localhost"
c.setting :port, 8080
c.setting :timeout do
# Computed value via block
ENV.fetch("TIMEOUT", 30).to_i
end
c.group :database do |db|
db[:adapter] = "postgresql"
db[:pool] = 5
end
end
puts config.settings
# => {:host=>"localhost", :port=>8080, :timeout=>30, :database=>{:adapter=>"postgresql", :pool=>5}}

Explanation: The configure method yields self, allowing the caller to call setting and group on the config object. When a block is passed to setting, the return value becomes the setting value. The group method creates a nested hash and yields it for further configuration.


Example 2: Implementing a Retry Decorator with Procs

Section titled “Example 2: Implementing a Retry Decorator with Procs”

Problem: Write a retry_on method that accepts both exception classes and a maximum retry count, using procs and lambdas for flexible error handling.

def retry_on(*exceptions, max_attempts: 3, delay: 1, &on_retry)
attempts = 0
begin
attempts += 1
yield
rescue *exceptions => e
on_retry&.call(e, attempts)
if attempts < max_attempts
sleep(delay * attempts)
retry
else
raise
end
end
end
# Usage with a lambda for custom retry logic
log_retry = ->(error, attempt) {
warn "[Attempt #{attempt}] #{error.class}: #{error.message}"
}
result = retry_on(TimeoutError, SocketError, max_attempts: 3, delay: 1, &log_retry) do
HTTP.get("https://api.example.com/data").parse
end

Explanation: *exceptions splats multiple exception classes into an array. &on_retry captures an optional proc. The lambda log_retry receives the error and attempt count, enabling custom logging. The delay doubles each attempt (exponential backoff).


Example 3: Event Emitter Pattern with Lambdas

Section titled “Example 3: Event Emitter Pattern with Lambdas”

Problem: Implement an event system where handlers can be registered with lambdas and triggered with arguments.

class EventEmitter
def initialize
@handlers = Hash.new { |h, k| h[k] = [] }
end
def on(event, &handler)
@handlers[event] << handler
self
end
def emit(event, *args)
@handlers[event].each { |h| h.call(*args) }
self
end
def off(event, &handler)
@handlers[event].delete(handler)
self
end
end
emitter = EventEmitter.new
emitter.on(:user_created) do |user|
puts "Welcome email sent to #{user[:email]}"
end
emitter.on(:user_created) do |user|
puts "Audit log: user #{user[:name]} created"
end
emitter.emit(:user_created, { name: "Alice", email: "alice@example.com" })
# => "Welcome email sent to alice@example.com"
# => "Audit log: user Alice created"

Explanation: Hash.new { |h, k| h[k] = [] } auto-creates empty arrays for new event keys. Each on call appends a lambda to the handler list. emit calls all handlers with the provided arguments. The method returns self to allow chaining.

  • OOP - How methods define the interface of Ruby objects and classes
  • Metaprogramming - How define_method and method_missing dynamically create methods
  • Variables and Types - How closures capture variables from their enclosing scope

Confusing blocks, procs, and lambdas. Blocks are not objects and cannot be stored in variables. Procs are anonymous functions that are lenient about argument count. Lambdas are anonymous functions that enforce strict argument checking. Students often treat them as interchangeable, but they behave differently with return and argument validation.

Forgetting that blocks do not create a new scope for variables. Variables defined inside a block are accessible in the enclosing scope (unlike methods which create a new scope). This can cause unexpected variable leakage. Use define inside a method to create a truly local variable.

Not understanding when to use yield vs &block. yield calls the block passed to the method. &block converts the block to a Proc object for storage or passing. Students often use yield when they need to pass the block to another method, or use &block when they just want to call it directly.