Methods
In Ruby, methods are functions defined with def — always associated with an object (the receiver). Methods admit several parameter forms: positional (with optional defaults), keyword (with optional defaults), splat (variadic positional *args), double-splat (variadic keyword **kwargs), and block (&block). The principal feature distinguishing Ruby methods from many languages: blocks. Almost every Ruby method admits an attached block — passed implicitly via the conventional do...end or { } syntax — which the method invokes via yield or stores via an explicit &block parameter. The combination — flexible parameter forms, blocks, the implicit self, the conventional implicit return — is the substance of Ruby’s method surface.
This page covers def-defined methods; the closure-style callables (Proc, lambda) are treated in Blocks and procs.
Method declarations
The principal form:
def name(parameters)
body
end
Examples:
def add(a, b)
a + b
end
def greet(name)
"Hello, #{name}"
end
def log_and_return(value)
puts value
value # implicit return
end
The parentheses around parameters are optional:
def add a, b # admitted; rare
a + b
end
The conventional discipline includes parentheses around parameters; admits omitting them only for argument-less methods (def foo).
Implicit return
The last expression in a method is the return value:
def square(n)
n * n # implicit return
end
def factorial(n)
return 1 if n <= 1 # explicit early return
n * factorial(n - 1) # implicit return
end
The conventional discipline:
- Implicit return for the principal return.
- Explicit
returnfor early returns and substantially complex flow.
Default arguments
def greet(name = "world")
"Hello, #{name}"
end
greet # "Hello, world"
greet("Alice") # "Hello, Alice"
def configure(host = "localhost", port = 8080, timeout = 30)
# ...
end
configure # all defaults
configure("example.com") # custom host
configure("example.com", 9000) # custom host and port
Default values may reference earlier parameters:
def make_url(host, port = 80, path = "/")
"http://#{host}:#{port}#{path}"
end
def process(items, count = items.length)
# ...
end
The mechanism admits substantial flexibility for sensible defaults.
Keyword arguments
Keyword arguments admit named parameters:
def configure(host:, port: 8080, timeout: 30)
puts "host: #{host}, port: #{port}, timeout: #{timeout}"
end
configure(host: "localhost") # OK; port and timeout default
configure(host: "example.com", port: 9000)
configure(port: 9000, host: "example.com") # order doesn't matter
configure # ERROR: missing keyword: host
The trailing : (with no default value) makes the keyword required; with a default value, it’s optional.
The conventional contemporary discipline uses keyword arguments substantially:
- Required keywords admit clear, self-documenting calls.
- Optional keywords admit substantial defaults without positional ambiguity.
- No order dependency — admits substantial refactoring.
Splat arguments
The *args collects extra positional arguments into an array:
def sum(*nums)
nums.reduce(0, :+)
end
sum # 0
sum(1, 2, 3) # 6
sum(*[10, 20, 30]) # 60 — spread an array
def first_then_rest(first, *rest)
puts "first: #{first}"
puts "rest: #{rest.inspect}"
end
first_then_rest(1, 2, 3, 4)
# first: 1
# rest: [2, 3, 4]
The splat may appear in any position (as long as parameters are unambiguous):
def example(first, *middle, last)
puts "first: #{first}, middle: #{middle.inspect}, last: #{last}"
end
example(1, 2, 3, 4, 5)
# first: 1, middle: [2, 3, 4], last: 5
Double-splat arguments
The **kwargs collects extra keyword arguments into a hash:
def configure(**opts)
opts.each { |k, v| puts "#{k} = #{v}" }
end
configure(host: "localhost", port: 8080, timeout: 30)
# host = localhost
# port = 8080
# timeout = 30
def with_required(name:, **opts)
puts "name: #{name}, opts: #{opts.inspect}"
end
with_required(name: "Alice", role: "admin", verbose: true)
# name: Alice, opts: {role: "admin", verbose: true}
The mechanism admits substantial flexibility for option-style methods.
Block arguments
A method may capture an attached block via &block:
def with_logging(&block)
puts "before"
result = block.call
puts "after"
result
end
with_logging { puts "in block" }
# before
# in block
# after
# Methods that take blocks usually use yield without an explicit & parameter:
def with_logging
puts "before"
result = yield
puts "after"
result
end
Treated in Blocks and procs.
Combined parameter forms
The full parameter list:
def example(req1, req2, opt1 = "default", *splat, kreq:, kopt: "x", **dsplat, &block)
# all forms in one definition
end
The order of parameter forms:
- Required positional (
a, b). - Optional positional with defaults (
c = "x"). - Splat (
*args). - Required keyword (
name:). - Optional keyword with defaults (
role: "user"). - Double-splat (
**opts). - Block (
&block).
The conventional discipline keeps parameter lists short; substantial parameter lists indicate a need for refactoring.
Visibility
class Service
def public_method
helper # OK
end
private
def helper
# ...
end
end
Service.new.public_method # OK
Service.new.helper # NoMethodError (private)
Treated in Classes and OOP.
Method aliasing
class Calculator
def add(a, b)
a + b
end
alias_method :sum, :add # convention
alias plus add # alternative form
end
c = Calculator.new
c.add(1, 2) # 3
c.sum(1, 2) # 3
c.plus(1, 2) # 3
The conventional alias_method form (a method) is more flexible than the alias keyword.
return, next, and break
The principal control-flow forms in methods:
def find(arr)
arr.each do |x|
return x if x.match? # returns from find
end
nil
end
def process(x)
return :empty if x.empty?
return :too_long if x.length > 100
do_work(x)
end
Inside a block (passed to a method), return returns from the enclosing method; next returns from the block; break exits the iterating method.
Method as object
A method may be obtained as an object via method(:name):
class Calculator
def add(a, b); a + b; end
end
c = Calculator.new
m = c.method(:add)
m.call(1, 2) # 3
m.(1, 2) # alternative call syntax
# Convert to Proc:
proc = c.method(:add).to_proc
[1, 2, 3].map(&proc) # interesting use
The mechanism admits substantial reflection.
Recursion
def factorial(n)
return 1 if n <= 1
n * factorial(n - 1)
end
def fib(n)
return n if n < 2
fib(n - 1) + fib(n - 2)
end
Ruby does not admit tail-call optimisation by default; the conventional defence for substantial recursion depth is iteration.
A pragma admits TCO under specific conditions:
RubyVM::InstructionSequence.compile_option = {
tailcall_optimization: true,
trace_instruction: false,
}
The mechanism is rarely used; the conventional contemporary discipline favours explicit iteration.
Variadic forwarding
Passing all arguments to another method:
def wrapper(*args, **opts, &block)
log_call(*args, **opts)
underlying_method(*args, **opts, &block)
end
# Or, since Ruby 2.7+, the unified ...:
def wrapper(...)
underlying_method(...)
end
The ... form admits substantial conciseness for forwarding methods.
Method lookup
Ruby’s method lookup follows the ancestor chain:
class Animal
def speak; "..."; end
end
module Walking
def walk; "walking"; end
end
class Dog < Animal
include Walking
def fetch; "fetching"; end
end
Dog.ancestors # [Dog, Walking, Animal, Object, Kernel, BasicObject]
The lookup proceeds: the singleton class, the class itself, then upward through prepended modules, included modules, the parent class, etc. The first match wins.
The mechanism admits substantial flexibility through mixin composition.
Common patterns
Builder method
def build_query(table, conditions: {}, order: nil, limit: nil)
query = "SELECT * FROM #{table}"
query += " WHERE " + conditions.map { |k, v| "#{k} = '#{v}'" }.join(" AND ") unless conditions.empty?
query += " ORDER BY #{order}" if order
query += " LIMIT #{limit}" if limit
query
end
build_query(:users, conditions: { active: true }, order: "created_at", limit: 10)
Default-via-||=
def fetch(url, options = {})
options[:timeout] ||= 30
options[:retries] ||= 3
# ...
end
The ||= admits “use the value if set; otherwise the default”.
Required keyword arguments
def create_user(name:, email:, role: "user", verified: false)
User.new(name: name, email: email, role: role, verified: verified)
end
# Calls are self-documenting:
create_user(name: "Alice", email: "a@b.c", role: "admin")
Method delegation with ...
class WrappedClient
def initialize(client)
@client = client
end
def fetch(...)
@client.fetch(...)
end
def post(...)
log_call
@client.post(...)
end
end
Variadic to_h
def merge_options(*sources, **explicit)
result = {}
sources.each { |h| result.merge!(h) }
result.merge!(explicit)
result
end
merge_options({ a: 1 }, { b: 2 }, c: 3, d: 4)
# { a: 1, b: 2, c: 3, d: 4 }
Optional block
def process(items)
items.each do |item|
result = transform(item)
yield result if block_given?
end
end
process(items) # no callback
process(items) { |r| log(r) } # with callback
The block_given? admits checking whether a block was passed.
Returning multiple values
def divmod(a, b)
[a / b, a % b] # returns array
end
q, r = divmod(17, 5) # destructure
Treated in Operators.
Method chaining
class Pipeline
def initialize(value)
@value = value
end
def map(&block)
@value = block.call(@value)
self
end
def filter(&block)
@value = nil unless block.call(@value)
self
end
def value
@value
end
end
result = Pipeline.new(42)
.map { |n| n * 2 }
.filter { |n| n > 50 }
.value
The pattern admits substantial fluent APIs.
Memoizing methods
class Service
def expensive_data
@expensive_data ||= compute_data
end
private
def compute_data
# ...
end
end
Instance method with class-method partner
class User
def self.find(id)
new(id: id, loaded: false).load!
end
def initialize(id:, loaded:)
@id = id
@loaded = loaded
end
def load!
# ...
@loaded = true
self
end
end
user = User.find(42)
Nested methods (admitted but rare)
def outer
def inner # define inner as a top-level method
"inner" # NOT scoped to outer
end
inner
end
Ruby’s method definition is not lexically scoped — def always defines a method on the current self. The conventional alternative for nested helpers is private methods or local lambdas:
def outer
helper = ->(x) { x * 2 }
helper.call(5)
end
Method-missing for proxies
class Proxy
def initialize(target)
@target = target
end
def method_missing(name, *args, **kwargs, &block)
log "calling #{name}"
@target.public_send(name, *args, **kwargs, &block)
end
def respond_to_missing?(name, include_private = false)
@target.respond_to?(name, include_private)
end
end
Treated in Metaprogramming.
A note on self.foo setters
A pitfall: setters require explicit self:
class Account
attr_accessor :balance
def deposit(amount)
balance = balance + amount # WRONG: creates a local variable
self.balance += amount # OK: calls the setter
end
end
The balance = ... form (without self.) is parsed as creating a local; the self.balance = is the conventional setter call.
A note on the conventional discipline
The contemporary Ruby method advice:
- Use parentheses around parameters; admit omission for argument-less methods.
- Use keyword arguments for substantial parameter lists.
- Use defaults freely.
- Use splats (
*,**) for variadic parameters. - Use blocks for callbacks and iteration.
- Use the
...forwarding (Ruby 2.7+) for proxy methods. - Use implicit return for the principal value.
- Use explicit
returnfor early returns. - Use
privategenerously — keep public surface small. - Use
||=for memoization. - Avoid nested
def— use lambdas or private methods. - Use
self.in setter calls.
The combination — flexible parameter forms (positional, default, keyword, splat, double-splat, block), implicit return, the ... forwarding, the substantial integration with blocks — is the substance of Ruby’s method surface. The discipline produces concise, expressive method definitions with substantial flexibility for the calling style.