Functions #

In Ruby, the accurate term isn’t “function” but “method” — because every callable piece of code is attached to an object, whether it’s a class instance, the class itself, or the top-level main object. But beyond terminology, methods in Ruby have several characteristics that distinguish them from functions in other languages: there’s always a return value (the last expression is returned implicitly), parentheses are optional when calling, and the parameter system is extremely flexible — from default values and keyword arguments to splat and double splat for hashes. Understanding all of this is the foundation for writing Ruby that’s expressive and easy for other developers to use.

Defining and Calling Methods #

Methods are defined with the def keyword and end with end. Method names follow the snake_case convention:

def greet(name)
  "Hello, #{name}!"
end

puts greet("Dewi")   # => Hello, Dewi!

Parentheses when calling a method are optional in Ruby — but there’s a convention to follow:

# Parentheses MAY be omitted when calling
puts greet "Dewi"   # valid
puts greet("Dewi")  # also valid

# CONVENTION: use parentheses when there are arguments, omit when there aren't
puts greet("Dewi")  # ← clearer that there are arguments
list_products      # ← no arguments, parentheses unnecessary

# ANTI-PATTERN: omitting parentheses when there are arguments — ambiguous
result = calculate width, height   # hard to distinguish from variables
result = calculate(width, height)  # clearly a method call

Implicit Return Values #

In Ruby, methods always return a value — namely the value of the last expression executed. The return keyword isn’t needed except for early exits:

# ANTI-PATTERN: explicit return at the end of a method — unnecessary
def square(n)
  return n ** 2   # ← return here isn't needed
end

# CORRECT: implicit return — the last expression is returned automatically
def square(n)
  n ** 2
end

# return IS useful for early exit
def divide(a, b)
  return "Error: cannot divide by zero" if b == 0
  a.fdiv(b)
end

puts divide(10, 2)   # => 5.0
puts divide(10, 0)   # => "Error: cannot divide by zero"

Because methods are expressions that return values, they can be used directly in any context:

def max(a, b)
  a > b ? a : b
end

puts max(3, 7)                         # => 7
puts "The largest value: #{max(10, 5)}"   # => The largest value: 10
result = [max(1,2), max(3,4), max(5,6)] # => [2, 4, 6]

Parameters and Arguments #

Ruby provides a very rich parameter system — far more flexible than most languages.

Positional Parameters #

Positional parameters are the most basic — values are passed by order:

def create_profile(name, age, city)
  "#{name}, #{age} years old, from #{city}"
end

puts create_profile("Rina", 28, "Bandung")
# => Rina, 28 years old, from Bandung

Default Parameters #

Parameters can have default values used when an argument isn’t given:

def send_message(to, content, format: :text, priority: :normal)
  "[#{priority.upcase}] To: #{to} | Format: #{format} | #{content}"
end

puts send_message("[email protected]", "Server down!")
# => [NORMAL] To: [email protected] | Format: text | Server down!

puts send_message("[email protected]", "Critical!", priority: :high, format: :html)
# => [HIGH] To: [email protected] | Format: html | Critical!

Default parameters can be any expression — including values computed at call time:

def log(message, time = Time.now, level = :info)
  "[#{level.upcase}] #{time.strftime('%H:%M:%S')}#{message}"
end

puts log("Server started")
puts log("Critical error", Time.now, :error)
Parameters with default values must come after parameters without defaults. Ruby technically allows a default in the middle, but it’s confusing — put all required parameters first, optional parameters last.

Keyword Arguments #

Keyword arguments make method calls more expressive because parameter names are stated explicitly. Order doesn’t matter, and there’s no ambiguity:

def create_connection(host:, port:, database:, timeout: 30, ssl: false)
  puts "Connecting to #{host}:#{port}/#{database}"
  puts "Timeout: #{timeout}s | SSL: #{ssl}"
end

# Order can be anything — the name determines it
create_connection(
  database: "app_production",
  host:     "db.example.com",
  port:     5432,
  ssl:      true
)

# ANTI-PATTERN: positional parameters for methods with many arguments
def create_connection_old(host, port, database, timeout, ssl)
  # the caller must remember the exact order — error-prone
end

create_connection_old("db.example.com", 5432, "app", 30, true)
# 5432, 30, true — what do these mean without names?

You can accept previously unknown keyword arguments with **:

def show_options(**options)
  options.each { |k, v| puts "  #{k}: #{v}" }
end

show_options(color: :blue, size: :large, bold: true)
# => color: blue
# => size: large
# => bold: true

The Splat Operator — Variable Arguments #

Splat (*) collects all remaining positional arguments into an Array:

def sum(*numbers)
  numbers.sum
end

puts sum(1, 2, 3)           # => 6
puts sum(10, 20, 30, 40)    # => 100
puts sum                    # => 0 (empty array)

# Splat in the middle — capture the "rest" of the arguments
def first_middle_last(first, *middle, last)
  puts "First: #{first}"
  puts "Middle: #{middle.inspect}"
  puts "Last: #{last}"
end

first_middle_last(1, 2, 3, 4, 5)
# => First: 1
# => Middle: [2, 3, 4]
# => Last: 5

Splat is also useful for passing an Array as separate arguments:

def add(a, b, c)
  a + b + c
end

numbers = [1, 2, 3]
puts add(*numbers)   # => 6  (splat "explodes" the array into separate arguments)

Combining All Parameter Types #

Ruby allows combining all parameter types — with an order that must be followed:

# Required order: positional → *splat → keyword → **double_splat → &block
def complex_method(required, optional = "default", *rest, keyword:, kw_optional: nil, **extra, &block)
  puts "required: #{required}"
  puts "optional: #{optional}"
  puts "rest: #{rest.inspect}"
  puts "keyword: #{keyword}"
  puts "kw_optional: #{kw_optional.inspect}"
  puts "extra: #{extra.inspect}"
  block.call if block
end

complex_method("a", "b", "c", "d", keyword: "required", x: 1, y: 2) { puts "block!" }

Methods with Blocks #

One of Ruby’s most distinctive features is the ability to pass a block of code to a method. There are two ways to receive and run a block: with yield and with an explicit &block parameter.

yield #

yield calls the block passed to the method. You can pass values to the block as yield arguments:

def time_it(label)
  start = Time.now
  result = yield   # call the block, capture its return value
  duration = Time.now - start
  puts "#{label} finished in #{(duration * 1000).round(2)}ms"
  result
end

total = time_it("Calculation") do
  (1..1_000_000).sum
end
puts "Result: #{total}"
# => Calculation finished in ~XXms
# => Result: 500000500000

# Passing values to the block
def transform(collection)
  collection.map { |item| yield item }
end

result = transform([1, 2, 3, 4, 5]) { |n| n ** 2 }
puts result.inspect   # => [1, 4, 9, 16, 25]

block_given? checks whether a block was passed — useful for making blocks optional:

def process(data)
  result = data.map { |d| d.to_s.upcase }

  if block_given?
    yield result      # give the block a chance to do something with the result
  else
    result            # return directly if there's no block
  end
end

process(["a", "b", "c"])                     # => ["A", "B", "C"]
process(["a", "b", "c"]) { |h| h.join(", ") }  # => "A, B, C"

Explicit Block Parameters with & #

If you need to store a block in a variable, pass it to another method, or call it more than once, use the & parameter:

def run_twice(&block)
  block.call
  block.call
end

run_twice { puts "Hello!" }
# => Hello!
# => Hello!

# Passing a block to another method
def filter_and_process(data, &condition)
  data.select(&condition).map { |d| d * 10 }
end

result = filter_and_process([1, 2, 3, 4, 5]) { |n| n.odd? }
puts result.inspect   # => [10, 30, 50]

&:method_name is the idiomatic shorthand for converting a Symbol into a block:

# ANTI-PATTERN: verbose blocks for simple methods
["rina", "budi", "citra"].map { |n| n.upcase }
["rina", "budi", "citra"].select { |n| n.start_with?("r") }

# CORRECT: Symbol to Proc — concise and expressive
["rina", "budi", "citra"].map(&:upcase)
["rina", "budi", "citra"].select(&:frozen?)
[1, -2, 3, -4].select(&:positive?)
[nil, 1, nil, 2].reject(&:nil?)

Method Visibility — public, private, protected #

Ruby controls method access through three visibility levels:

class BankAccount
  def initialize(balance)
    @balance = balance
  end

  # public — callable from anywhere (default)
  def info
    "Balance: #{format_rupiah(@balance)}"
  end

  def transfer(to, amount)
    return "Insufficient balance" unless sufficient_balance?(amount)
    reduce_balance(amount)
    to.add_balance(amount)
    "Transfer successful"
  end

  protected

  # protected — callable by instances of the same class
  def add_balance(amount)
    @balance += amount
  end

  private

  # private — only callable from within this class itself
  def format_rupiah(number)
    "Rp #{number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1.').reverse}"
  end

  def sufficient_balance?(amount)
    @balance >= amount
  end

  def reduce_balance(amount)
    @balance -= amount
  end
end

account = BankAccount.new(1_000_000)
puts account.info              # => Balance: Rp 1.000.000
account.reduce_balance(100)    # => NoMethodError: private method called
account.format_rupiah(5000)    # => NoMethodError: private method called
flowchart TD
    A[Method Visibility] --> B[public]
    A --> C[protected]
    A --> D[private]
    B --> B1["Callable from anywhere\nOther objects, outside the class"]
    C --> C1["Only from within the class\nand its subclasses\nUseful for operators like <=>"]
    D --> D1["Only from within\nthe object itself\nwithout an explicit receiver"]
In modern Ruby (since 2.7), private can be used directly as a prefix: private def internal_calculation; end. This is more concise than placing the method after the private keyword below, and easier to read because the visibility intent is visible right next to the method definition.

Method Chaining #

Method chaining allows calling several methods in sequence within one expression. The key is that each method must return a relevant object — usually self:

class QueryBuilder
  def initialize
    @conditions = []
    @order      = nil
    @limit      = nil
  end

  def where(condition)
    @conditions << condition
    self   # return self for chaining
  end

  def order(column)
    @order = column
    self
  end

  def limit(n)
    @limit = n
    self
  end

  def build
    sql = "SELECT * FROM users"
    sql += " WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
    sql += " ORDER BY #{@order}" if @order
    sql += " LIMIT #{@limit}" if @limit
    sql
  end
end

query = QueryBuilder.new
          .where("active = true")
          .where("age > 18")
          .order("name ASC")
          .limit(10)
          .build

puts query
# => SELECT * FROM users WHERE active = true AND age > 18 ORDER BY name ASC LIMIT 10

Method chaining is very natural in Ruby because almost all Enumerable methods return a new collection that can be chained directly:

result = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
  .uniq
  .select { |n| n > 2 }
  .sort
  .map { |n| n ** 2 }
  .first(3)

puts result.inspect   # => [9, 16, 25]

Method Naming Conventions #

Ruby has strong, meaningful naming conventions — a method’s suffix conveys information about its behavior:

# Methods that return booleans — ? suffix
user.active?          # => true / false
string.empty?         # => true / false
array.include?(5)     # => true / false
number.zero?          # => true / false

# Dangerous / object-modifying methods — ! suffix
name.upcase           # returns a new String
name.upcase!          # modifies name itself (destructive)

array.sort            # returns a new sorted Array
array.sort!           # sorts the array in place

string.strip          # returns a new String without whitespace
string.strip!         # modifies the string itself

# Setter methods — = suffix
class Product
  def name=(value)     # setter: product.name = "Laptop"
    @name = value
  end

  def name             # getter: product.name
    @name
  end
end
Method naming convention guide:
  ? suffix  → predicate method, always returns a boolean
  ! suffix  → destructive or "dangerous" version of a regular method
  = suffix  → setter method, called with assignment syntax
  is_ prefix → ANTI-PATTERN in Ruby — use ? not is_active?
  get_/set_ prefix → ANTI-PATTERN in Ruby — use the name directly

Methods at the Top Level #

Methods defined outside any class or module actually become private methods of the main object (an instance of Object). This makes them look like global functions:

def hello
  puts "Hello from the top level!"
end

def add(a, b)
  a + b
end

hello              # => Hello from the top level!
puts add(3, 4)     # => 7

# Behind the scenes, this is equivalent to:
# Object.send(:define_method, :hello) { puts "Hello!" }
# That's why top-level methods can be called from anywhere

Proc and Lambda as First-class Functions #

Ruby treats blocks, Procs, and Lambdas as first-class objects — they can be stored in variables, passed as arguments, and returned from methods. This opens up powerful functional programming patterns.

# Lambda — an anonymous method storable in a variable
square  = ->(n) { n ** 2 }
add     = ->(a, b) { a + b }
greet   = ->(name, greeting: "Hello") { "#{greeting}, #{name}!" }

puts square.call(5)         # => 25
puts square.(5)             # alternative syntax
puts square[5]              # third alternative syntax

puts add.call(3, 4)         # => 7
puts greet.call("Rina")     # => "Hello, Rina!"
puts greet.call("Budi", greeting: "Welcome")

# Lambda as an argument
def apply(value, transformation)
  transformation.call(value)
end

puts apply(10, square)              # => 100
puts apply(10, ->(n) { n + 5 })     # => 15

# Higher-order methods — return a lambda from a method
def multiplier_factory(factor)
  ->(n) { n * factor }
end

times_two   = multiplier_factory(2)
times_ten   = multiplier_factory(10)

puts times_two.call(7)       # => 14
puts times_ten.call(7)       # => 70

# Function composition
def compose(f, g)
  ->(x) { f.call(g.call(x)) }
end

add_one = ->(n) { n + 1 }
times_three = ->(n) { n * 3 }

add_then_multiply = compose(times_three, add_one)
puts add_then_multiply.call(4)   # => (4+1)*3 = 15

Method Objects #

You can also take a reference to an existing method as an object using method():

def power_of_two(n)
  n ** 2
end

fn = method(:power_of_two)
puts fn.call(6)              # => 36
puts [1, 2, 3, 4].map(&fn).inspect   # => [1, 4, 9, 16]

# Very useful for passing methods to iterators
["  Rina  ", "  Budi  ", " Citra "].map(&method(:puts))
# prints each name

validator = method(:valid_email?)
emails.select(&validator)   # filter using an existing method

Memoization in Methods #

The memoization pattern uses ||= to compute an expensive value only once and store it in an instance variable:

class Report
  def initialize(year)
    @year = year
  end

  def total_revenue
    @total_revenue ||= calculate_revenue
  end

  def total_expenses
    @total_expenses ||= calculate_expenses
  end

  def net_profit
    total_revenue - total_expenses
  end

  private

  def calculate_revenue
    puts "Calculating revenue from DB..."
    # expensive query — only run once
    rand(1_000_000_000)
  end

  def calculate_expenses
    puts "Calculating expenses from DB..."
    rand(500_000_000)
  end
end

report = Report.new(2024)
puts report.total_revenue   # "Calculating..." appears
puts report.total_revenue   # cached — "Calculating..." doesn't appear again
puts report.net_profit      # uses the already-cached values

Summary #

  • There’s always a return value — the last expression in a method is returned implicitly. Use return only for early exits, not at the end of a method.
  • Keyword arguments are more expressive than positional — for methods with more than two or three arguments, keyword arguments prevent order confusion and clarify the caller’s intent.
  • Splat * for variable arguments — collect unlimited positional arguments into an Array. Double splat ** for unlimited keyword arguments into a Hash.
  • yield for simple blocks, &block for blocks that need storing — use block_given? to make blocks optional.
  • &:method_name is the most concise idiom for passing simple methods to iterators.
  • ? suffix for booleans, ! for destructive, = for setters — this isn’t just cosmetic convention; it communicates a method’s behavioral contract.
  • private for internal implementation — hide implementation details so the class API stays clean and you’re free to change internals without breaking users.
  • Method chaining with return self — return self from state-modifying methods to enable expressive chaining.
  • Lambdas can be stored in variables and passed around — use them for higher-order functions, composition, and callbacks that need to survive more than one call cycle.
  • Memoization with ||= — store expensive calculation results in instance variables so they aren’t recomputed on every call.

← Previous: Loops   Next: Classes →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact