Methods and Blocks
Intuition
Section titled “Intuition”Methods and blocks are Ruby’s building blocks for code organisation. Methods encapsulate reusable logic, and blocks provide closures that capture their surrounding context. The interplay between methods, procs, and lambdas creates a flexible system where behaviour can be passed as data. Blocks enable the iterator pattern that Ruby uses extensively, replacing traditional loops with expressive method calls like map, select, and reduce.
Method Definition
Section titled “Method Definition”Basic Methods
Section titled “Basic Methods”## Simple method definitiondef greet puts "Hello, World!"end
greet # => prints "Hello, World!"
## Method with parametersdef 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 valueend
# Implicit return (idiomatic)def add(a, b) a + bend
# Explicit returndef add(a, b) return a + bend
# Return vs last expressiondef example return 42 if some_condition "default value"end
# Method with multiple expressionsdef 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 }endDefault Parameters
Section titled “Default Parameters”# Default parameter valuesdef greet(name = "World") "Hello, #{name}!"end
greet # => "Hello, World!"greet("Alice") # => "Hello, Alice!"
# Multiple defaultsdef 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 parametersdef create_range(start_val = 0, end_val = start_val + 10) (start_val..end_val)end
# Be careful with mutable defaults# BAD -- same array is reuseddef add_item(items = []) items << "new" itemsend
# GOOD -- use nil and create insidedef add_item(items = nil) items ||= [] items << "new" itemsendKeyword Arguments
Section titled “Keyword Arguments”# Basic keyword argumentsdef configure(host:, port:, timeout:) puts "#{host}:#{port} (#{timeout}s)"end
configure(host: "localhost", port: 8080, timeout: 30)
# Keyword arguments with defaultsdef 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 argumentsdef 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 argumentsdef accept_any(**kwargs) kwargsend
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 throughdef wrapper(host:, port:, **other_options) actual_connect(host: host, port: port, **other_options)endSplat Arguments
Section titled “Splat Arguments”# Splat operator * collects remaining positional arguments into an arraydef sum(*numbers) numbers.reduce(0, :+)end
sum(1, 2, 3) # => 6sum(1, 2, 3, 4, 5) # => 15
# Splat with required argumentsdef log(level, *messages) messages.each { |msg| puts "[#{level}] #{msg}" }end
log("INFO", "Server started", "Listening on port 8080")
# Splat to expand an array into argumentsdef add(a, b, c) a + b + cend
numbers = [1, 2, 3]add(*numbers) # => 6
# Splat in the middledef 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 argumentsdef options(**opts) opts.each { |k, v| puts "#{k}: #{v}" }end
options(color: "red", size: "large")
# Splat and double splat togetherdef 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 and Overriding
Section titled “Method Aliasing and Overriding”# Method aliasingclass String alias :sentence_case :capitalizeend
"hello world".sentence_case # => "Hello world"
# Override a method while preserving the originalclass Array def sum reduce(0) { |acc, elem| acc + (elem.is_a?(Numeric) ? elem : 0) } endend
# Prevent overridingclass String freezeendReturn Values
Section titled “Return Values”# Last expression is the return valuedef multiply(a, b) a * bendmultiply(3, 4) # => 12
# Explicit return exits immediatelydef first_positive(numbers) numbers.each do |n| return n if n > 0 end nil # default return if loop completes without findingend
# 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 # => 1puts 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 blockdef find_even(array) array.each do |n| return n if n.even? # returns from find_even, not from the block end nilend
# next returns from the block onlydef find_even_with_next(array) array.each do |n| next if n.odd? return n # only reached for even numbers end nilendBlocks
Section titled “Blocks”Blocks are anonymous chunks of code that can be passed to methods. They are one of Ruby”s most powerful features.
Block Syntax
Section titled “Block Syntax”# Block with do..end (multi-line convention)[1, 2, 3].each do |n| puts nend
# 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 parameters3.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 blockdef greet puts "Before yield" yield puts "After yield"end
greet { puts "Inside block" }# Output:# Before yield# Inside block# After yield
# Yield with argumentsdef each_item(items) items.each { |item| yield(item) }end
each_item([1, 2, 3]) { |n| puts n * 10 }# => 10, 20, 30
# Yield with multiple argumentsdef pairs yield(1, "a") yield(2, "b") yield(3, "c")end
pairs { |number, letter| puts "#{number}: #{letter}" }
# Check if a block was givendef maybe_yield if block_given? yield else puts "No block provided" endend
maybe_yield { puts "Block here" } # => "Block here"maybe_yield # => "No block provided"Block vs Method with yield
Section titled “Block vs Method with yield”# Custom iterator using yielddef my_each(array) index = 0 while index < array.length yield(array[index]) index += 1 endend
my_each([10, 20, 30]) { |n| puts n }
# Custom map using yielddef my_map(array) result = [] array.each { |element| result << yield(element) } resultend
my_map([1, 2, 3]) { |n| n ** 2 } # => [1, 4, 9]
# Custom select using yielddef my_select(array) result = [] array.each { |element| result << element if yield(element) } resultend
my_select([1, 2, 3, 4, 5]) { |n| n.even? } # => [2, 4]Capturing Blocks as Procs
Section titled “Capturing Blocks as Procs”# & converts a block to a Proc and vice versadef run_twice(&block) block.call block.callend
run_twice { puts "hello" }# => "hello" (printed twice)
# Explicit block parameterdef 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 Procgreeter = Proc.new { |name| puts "Hello, #{name}!" }greeter.call("Alice") # => "Hello, Alice!"greeter.("Alice") # shorthandgreeter["Alice"] # also works
# Proc.new with a blockmy_proc = Proc.new { puts "I am a proc" }
# proc methodanother_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 arityflexible = 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 methoddef 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 environmentdef counter count = 0 increment = Proc.new { count += 1 } incrementend
c = counterc.call # => 1c.call # => 2c.call # => 3Lambdas
Section titled “Lambdas”Lambdas are a special type of Proc with strict argument checking and return semantics:
# Creating a lambdagreet = -> (name) { puts "Hello, #{name}!" }greet.call("Alice") # => "Hello, Alice!"
# Shorthand lambdasquare = ->(n) { n ** 2 }square.call(5) # => 25
# Multi-line lambdacalculate = ->(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 aritystrict = ->(a, b) { a + b }strict.call(1, 2) # => 3strict.call(1) # => ArgumentError (wrong number of arguments)strict.call(1, 2, 3) # => ArgumentError
# Lambda vs Proc: return semanticsdef 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 methodProc vs Lambda Summary
Section titled “Proc vs Lambda Summary”| Feature | Proc | Lambda |
|---|---|---|
| Creation | Proc.new {} or proc {} | -> {} or lambda {} |
| Arity check | Lenient (fills with nil) | Strict (raises ArgumentError) |
| Return | Returns from enclosing method | Returns from lambda only |
lambda? | false | true |
# Checking the typep = Proc.new {}l = lambda {}
p.lambda? # => falsel.lambda? # => true
# Both are Proc objectsp.class # => Procl.class # => Proc
# Practical exampleclass Event def initialize @handlers = [] end
def on(&handler) @handlers << handler end
def trigger(*args) @handlers.each { |h| h.call(*args) } endend
button = Event.newbutton.on { puts "Clicked!" }button.on { |x, y| puts "Clicked at (#{x}, #{y})" }button.trigger(100, 200)Method Objects
Section titled “Method Objects”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 endend
calc = Calculator.new
# Convert method to Method objectadd_method = calc.method(:add)add_method.call(3, 4) # => 7add_method.call(10, 20) # => 30add_method.arity # => 2
# UnboundMethod -- method without a receiverunbound = Calculator.instance_method(:add)bound = unbound.bind(calc)bound.call(3, 4) # => 7
# Bind to a different objectcalc2 = Calculator.newbound2 = unbound.bind(calc2)bound2.call(5, 6) # => 11
# Method introspectionadd_method.name # => :addadd_method.owner # => Calculatoradd_method.receiver # => #<Calculator:...>add_method.parameters # => [[:req, :a], [:req, :b]]add_method.source_location # => ["/path/to/file.rb", 2]
# Convert Method to Procadd_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
Section titled “define_method”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}!" endend
p = Person.newp.name = "Alice"p.name # => "Alice"p.greet # => "Hello, Alice!"p.greet("Hi") # => "Hi, Alice!"
# define_method with a Procclass 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 lambdaclass Formatter define_method(:format_price, ->(amount) do "$#{'%.2f' % amount}" end)end
Formatter.new.format_price(42.5) # => "$42.50"
# Dynamic attribute methodsclass 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 endend
class User < Model attributes :name, :email, :ageend
u = User.new(name: "Alice", email: "a@b.com")u.name? # => trueu.email # => "a@b.com"method_missing
Section titled “method_missing”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 endend
obj = DynamicAccess.new(name: "Alice", age: 30)obj.name # => "Alice"obj.age # => 30obj.city = "NYC"obj.city # => "NYC"obj.missing # => NoMethodError
# respond_to? works because we defined respond_to_missing?obj.respond_to?(:name) # => trueobj.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 endend
ar = FakeActiveRecord.newar.find_by_name("Bob") # => { id: 2, name: "Bob", role: "user" }ar.find_by_role("admin") # => { id: 1, name: "Alice", role: "admin" }Safe Method Missing
Section titled “Safe Method Missing”# Always define respond_to_missing? with method_missingclass 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 endend
# 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 introspectionBlock Variable Scope
Section titled “Block Variable Scope”# Blocks are closures -- they capture variables from enclosing scopex = 10y = 20
[1, 2, 3].each do |n| puts "#{n}, #{x}" # x is accessibleend
# Blocks can modify enclosing variablescounter = 0[1, 2, 3].each do |n| counter += nendputs 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_nend
# Thread-safety with blocksshared = []mutex = Mutex.new
[1, 2, 3].each do |n| mutex.synchronize do shared << n * 2 endendPractical Patterns
Section titled “Practical Patterns”Builder Pattern with Blocks
Section titled “Builder Pattern with Blocks”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 endend
html = HTMLBuilder.new.build do |b| b.tag(:div, class: "container") do b.tag(:h1) { b.text("Title") } b.tag(:p) { b.text("Content") } endendDecorator Pattern with Blocks
Section titled “Decorator Pattern with Blocks”def time_it start = Time.now result = yield elapsed = Time.now - start puts "Took #{elapsed.round(4)}s" resultend
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 endend
result = retry_on(TimeoutError) { fetch_data }Memoization with Blocks
Section titled “Memoization with Blocks”class Cache def initialize @store = {} end
def fetch(key) return @store[key] if @store.key?(key)
@store[key] = yield endend
cache = Cache.newcache.fetch("expensive") { compute_expensive_result }cache.fetch("expensive") { compute_expensive_result } # returns cached valueCross-References
Section titled “Cross-References”- Variables and Types defines the data types that methods accept as parameters and return as values.
- Control Flow provides the conditional logic used within method bodies and block iterations.
- Object-Oriented Programming shows how methods define object behaviour in Ruby’s class-based system.
Common Mistakes
Section titled “Common Mistakes”- Confusing Procs and Lambdas: A
Proctreats the entire method body as its block, soreturnexits the enclosing method. A lambda behaves like a method —returnexits only the lambda. Use lambdas (->) when you need strict arity checking and proper return behaviour. - Forgetting that blocks are not objects: You cannot assign a block to a variable directly. Use
Proc.newor&blockin the method parameter to capture a block, then call it with.callor.yield. - Using
yieldand&blocktogether incorrectly: If you declare&blockin the method signature, Ruby converts the block to a Proc. Usingyieldafter that raises an error. Useblock.callinstead when you have captured the block. - Ignoring the last-evaluated expression rule: Ruby methods implicitly return the value of the last expression. Accidentally including an extra statement can change the return value and introduce subtle bugs.