ZetCode

Ruby Functions

last modified April 2, 2025

Functions (methods) are fundamental building blocks in Ruby programming. This guide covers everything from basic method definition to advanced techniques like blocks, procs, and lambdas. Learn to create flexible, reusable code with proper parameter handling and scope management. Mastering Ruby functions is essential for writing clean, maintainable code.

Basic Method Definition

Ruby methods are defined with the def keyword followed by the method name. Parentheses for parameters are optional in definition and calls. Methods return the value of their last expression unless an explicit return is used. This example shows basic method syntax and calling conventions. Understanding these fundamentals is crucial for Ruby development.

basic_methods.rb
# Simple method without parameters
def greet
  "Hello, world!"
end

# Method with parameters (parentheses optional)
def add_numbers num1, num2
  num1 + num2
end

# Method with explicit return
def max a, b
  return a if a > b
  b
end

puts greet                # => "Hello, world!"
puts add_numbers(3, 4)    # => 7
puts add_numbers 5, 6     # => 11 (parentheses optional)
puts max(10, 20)          # => 20

# Additional example: Method with default return
def no_return
  "This string is returned"
end
puts no_return            # => "This string is returned"

Ruby methods are flexible in their definition and invocation syntax. The greet method shows a parameter-less definition, while add_numbers demonstrates parameter handling. The max method uses an explicit return statement to exit early.

Method calls can use parentheses or omit them when unambiguous. The last expression's value is automatically returned, as shown in the no_return example. This implicit return behavior makes Ruby code concise while remaining clear.

Method Parameters

Ruby offers several parameter types: required, optional, default-valued, and variable-length. Proper parameter handling makes methods more flexible and robust. This example demonstrates different parameter patterns in Ruby methods. Understanding these options helps create adaptable method interfaces.

parameters.rb
# Required parameters
def full_name(first, last)
  "#{first} #{last}"
end

# Default parameter values
def greet(name, greeting = "Hello")
  "#{greeting}, #{name}!"
end

# Variable-length arguments
def sum(*numbers)
  numbers.sum
end

# Keyword arguments (Ruby 2.0+)
def create_person(name:, age:, email: nil)
  { name: name, age: age, email: email }
end

puts full_name("John", "Doe")         # => "John Doe"
puts greet("Alice")                   # => "Hello, Alice!"
puts greet("Bob", "Hi")               # => "Hi, Bob!"
puts sum(1, 2, 3)                     # => 6
puts sum(4, 5, 6, 7)                  # => 22

person = create_person(name: "Eve", age: 30)
puts person.inspect                   # => {:name=>"Eve", :age=>30, :email=>nil}

# Additional example: Mixed parameters
def mixed(a, b = 2, *c, d:, e: 5)
  { a: a, b: b, c: c, d: d, e: e }
end
puts mixed(1, 3, 4, 5, d: 6).inspect # => {:a=>1, :b=>3, :c=>[4, 5], :d=>6, :e=>5}

Required parameters like first and last must always be provided. Default parameters (greeting = "Hello") make arguments optional. The splat operator (*) collects variable arguments into an array, as shown in sum.

Keyword arguments (Ruby 2.0+) provide named parameters with optional defaults (email: nil). The mixed example combines all types: required, default, variable-length, and keyword parameters. This flexibility allows for highly customizable method signatures.

Return Values and Multiple Returns

Ruby methods can return multiple values, which are automatically converted to an array. The return keyword is optional but useful for early exits. This example demonstrates various return value patterns. Understanding return behavior helps write more expressive methods.

returns.rb
# Single value return
def square(x)
  x * x
end

# Multiple values return (as array)
def min_max(numbers)
  [numbers.min, numbers.max]
end

# Early return
def safe_divide(a, b)
  return "Cannot divide by zero" if b == 0
  a / b
end

# Implicit vs explicit return
def implicit_return
  "This is returned"
end

def explicit_return
  return "This is returned"
  "This is not"
end

puts square(5)                    # => 25
puts min_max([3, 1, 4, 2]).inspect # => [1, 4]
puts safe_divide(10, 2)           # => 5
puts safe_divide(10, 0)           # => "Cannot divide by zero"
puts implicit_return               # => "This is returned"
puts explicit_return               # => "This is returned"

# Additional example: Destructuring multiple returns
min, max = min_max([8, 3, 5, 2])
puts "Min: #{min}, Max: #{max}"   # => "Min: 2, Max: 8"

The square method shows simple single-value return. min_max returns multiple values as an array, which can be destructured by the caller. safe_divide demonstrates early return for error handling.

Ruby always returns the last evaluated expression, making return optional. However, explicit returns improve clarity in complex methods. The example shows how multiple return values can be conveniently assigned to separate variables.

Blocks and Yield

Blocks are anonymous code chunks passed to methods, enabling powerful customization. The yield keyword executes a passed block. This example demonstrates block usage and control flow. Mastering blocks is key to leveraging Ruby's expressive power.

blocks.rb
# Simple block usage
def with_logging
  puts "Starting execution"
  yield
  puts "Completed execution"
end

with_logging { puts "Inside the block" }

# Block with parameters
def repeat(times)
  times.times { yield }
end

repeat(3) { puts "Hello!" }

# Block with return value
def transform(number)
  yield(number)
end

result = transform(5) { |x| x * 2 }
puts "Transformed result: #{result}" # => 10

# Checking for block given
def optional_block
  if block_given?
    yield
  else
    puts "No block provided"
  end
end

optional_block                  # => "No block provided"
optional_block { puts "Block!" } # => "Block!"

# Additional example: Block with multiple statements
def benchmark
  start = Time.now
  yield
  finish = Time.now
  puts "Execution time: #{finish - start} seconds"
end

benchmark { sleep(1) } # => "Execution time: ~1.0 seconds"

The with_logging method wraps block execution with messages. repeat demonstrates parameter passing to blocks. transform shows how blocks can return values to the calling method.

block_given? checks if a block was provided, enabling optional blocks. The benchmark example measures block execution time, showing how blocks can contain multiple statements. Blocks are fundamental to Ruby's iterators and DSL capabilities.

Procs and Lambdas

Procs and lambdas are objectified blocks that can be stored and passed around. They differ in argument handling and return behavior. This example compares these callable objects. Understanding them unlocks advanced Ruby patterns.

procs_lambdas.rb
# Creating a Proc
square = Proc.new { |x| x * x }
puts square.call(5)      # => 25

# Creating a Lambda
cube = lambda { |x| x ** 3 }
puts cube.call(3)        # => 27

# Alternative Lambda syntax
double = ->(x) { x * 2 }
puts double.call(4)      # => 8

# Differences in return behavior
def proc_return
  my_proc = Proc.new { return "Exiting proc" }
  my_proc.call
  "This won't be reached"
end

def lambda_return
  my_lambda = lambda { return "Exiting lambda" }
  my_lambda.call
  "This will be reached"
end

puts proc_return         # => "Exiting proc"
puts lambda_return       # => "This will be reached"

# Additional example: Passing procs to methods
def apply_operation(x, operation)
  operation.call(x)
end

puts apply_operation(10, square)  # => 100
puts apply_operation(10, cube)    # => 1000
puts apply_operation(10, double)  # => 20

Procs are created with Proc.new or the proc method. Lambdas use lambda or the stabby arrow syntax (->). Both are called with call or [].

Key differences: lambdas check argument count while procs don't, and return in a proc exits the enclosing method, not just the proc. The example shows how both can be passed as arguments to other methods.

Method Objects

Ruby methods can be converted to Method objects using method, allowing them to be passed around like procs. This example demonstrates method objects and their usage. This technique enables higher-order function patterns.

method_objects.rb
class Calculator
  def add(a, b)
    a + b
  end

  def multiply(a, b)
    a * b
  end
end

calc = Calculator.new
add_method = calc.method(:add)
multiply_method = calc.method(:multiply)

puts add_method.call(3, 4)        # => 7
puts multiply_method.call(3, 4)   # => 12

# Converting to proc
add_proc = add_method.to_proc
puts [1, 2, 3].map(&add_proc.curry(2).call(10)) # => [11, 12, 13]

# Additional example: Unbound methods
unbound_add = Calculator.instance_method(:add)
new_calc = Calculator.new
bound_add = unbound_add.bind(new_calc)
puts bound_add.call(5, 6)         # => 11

Method objects preserve their receiver and can be called later with call. The example shows extracting methods from a Calculator instance and invoking them later.

Methods can be converted to procs with to_proc, enabling use with methods like map. Unbound methods (without a receiver) can be bound to new instances. This flexibility supports advanced metaprogramming patterns.

Scope and Visibility

Ruby methods have different visibility levels: public, protected, and private. Instance variables have object scope, while local variables are method-scoped. This example demonstrates scope rules and visibility modifiers.

scope.rb
class BankAccount
  def initialize(balance)
    @balance = balance
  end

  # Public method
  def deposit(amount)
    @balance += amount
    log_transaction("Deposit: #{amount}")
  end

  # Another public method
  def withdraw(amount)
    if amount <= @balance
      @balance -= amount
      log_transaction("Withdrawal: #{amount}")
    else
      puts "Insufficient funds"
    end
  end

  # Protected method
  protected
  def log_transaction(message)
    puts "[LOG] #{message}"
  end

  # Private method
  private
  def secret_processing
    puts "Secret bank processing"
  end
end

account = BankAccount.new(100)
account.deposit(50)      # Works (public)
# account.log_transaction("Test") # Error (protected)
# account.secret_processing      # Error (private)

# Additional example: Local variable scope
def scope_demo
  local_var = "I'm local"
  puts local_var
end

scope_demo               # => "I'm local"
# puts local_var         # Error: undefined local variable

Public methods are accessible anywhere, protected methods can only be called by the class or its subclasses, and private methods can only be called within the class context. The example shows proper encapsulation with transaction logging.

Instance variables (@balance) persist across method calls within an object. Local variables (local_var) exist only within their method. Understanding these scope rules prevents variable leakage and naming collisions.

Functional Programming Techniques

Ruby supports functional programming patterns like higher-order functions, currying, and composition. This example demonstrates functional approaches to problem solving. These techniques lead to more declarative, reusable code.

functional.rb
# Higher-order function
def apply_math(x, y, operation)
  operation.call(x, y)
end

add = ->(a, b) { a + b }
multiply = ->(a, b) { a * b }

puts apply_math(3, 4, add)       # => 7
puts apply_math(3, 4, multiply)  # => 12

# Currying
power = ->(x, y) { x ** y }.curry
square = power.call(2)
cube = power.call(3)

puts square.call(5)              # => 25
puts cube.call(3)                # => 27

# Method composition
def double(x) = x * 2
def increment(x) = x + 1

composed = method(:double).to_proc >> method(:increment).to_proc
puts composed.call(5)            # => 11 (double then increment)

# Additional example: Recursion
def factorial(n)
  return 1 if n <= 1
  n * factorial(n - 1)
end

puts factorial(5)                # => 120

Higher-order functions accept or return other functions, as shown in apply_math. Currying (partial application) creates specialized functions from general ones, like square from power.

Method composition chains operations together (>> for right-to- left). Recursion is demonstrated with factorial, though Ruby lacks tail call optimization by default. These patterns provide powerful abstraction tools.

Method Missing and Dynamic Methods

Ruby's method_missing hook and define_method enable dynamic method handling and creation. This example shows metaprogramming techniques for flexible APIs. These advanced features power many Ruby DSLs.

dynamic.rb
class DynamicGreeter
  # method_missing for undefined methods
  def method_missing(name, *args)
    if name.to_s.start_with?('greet_')
      language = name.to_s.split('_').last
      "#{language.capitalize} greeting: Hello!"
    else
      super
    end
  end

  # respond_to_missing? should accompany method_missing
  def respond_to_missing?(name, include_private = false)
    name.to_s.start_with?('greet_') || super
  end

  # Dynamically define methods
  ['morning', 'afternoon', 'evening'].each do |time|
    define_method("greet_#{time}") do
      "Good #{time}!"
    end
  end
end

greeter = DynamicGreeter.new
puts greeter.greet_spanish      # => "Spanish greeting: Hello!"
puts greeter.greet_morning      # => "Good morning!"
puts greeter.greet_evening      # => "Good evening!"

# Additional example: send for dynamic dispatch
puts greeter.send(:greet_afternoon) # => "Good afternoon!"

method_missing catches calls to undefined methods, enabling dynamic responses like language-specific greetings. respond_to_missing? ensures proper method introspection.

define_method creates methods programmatically, as shown with time- based greetings. send dynamically invokes methods by name. These techniques enable flexible, adaptable APIs but should be used judiciously.

Best Practices

Use meaningful method names that indicate their purpose and side effects. Prefer small, single-responsibility methods over large complex ones. Use keyword arguments for methods with more than two parameters. Document methods with comments describing their purpose, parameters, and return values. Consider visibility levels (public/protected/private) to enforce proper encapsulation.

Source References

Learn more from these resources: Ruby Method Documentation, Proc Documentation, and Method Syntax.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.