Polyglot
Languages Ruby functions
Ruby § functions

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 return for 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:

  1. Required positional (a, b).
  2. Optional positional with defaults (c = "x").
  3. Splat (*args).
  4. Required keyword (name:).
  5. Optional keyword with defaults (role: "user").
  6. Double-splat (**opts).
  7. 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 scopeddef 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 return for early returns.
  • Use private generously — 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.