Methods and Blocks | Ruby - Wyatt's Notes
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 valueflowchart 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]Intuition
Section titled “Intuition”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.
Worked Examples
Section titled “Worked Examples”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 endend
config = Config.newconfig.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 endend
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 endend
# Usage with a lambda for custom retry logiclog_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").parseendExplanation: *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 endend
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.
Cross-References
Section titled “Cross-References”- 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
Common Mistakes
Section titled “Common Mistakes”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.