CrackedRuby CrackedRuby

Overview

Functions and procedures represent the fundamental mechanism for organizing code into reusable, named blocks that perform specific tasks. A function accepts input parameters, executes a sequence of operations, and returns a result. A procedure performs actions but may not return a meaningful value, focusing instead on side effects such as modifying state or producing output.

The distinction between functions and procedures originated in early programming languages. Functions derive from mathematical functions, taking inputs and producing outputs without side effects. Procedures derive from imperative programming, executing sequences of commands that modify program state. Modern languages often blur this distinction, with Ruby treating all callable code blocks as methods that can both return values and produce side effects.

Functions and procedures solve the problem of code duplication by extracting repeated logic into named, reusable units. They enable abstraction by hiding implementation details behind clear interfaces. They support modularity by breaking complex programs into manageable pieces that can be developed, tested, and maintained independently.

# Function: returns a value, no side effects
def calculate_area(radius)
  Math::PI * radius ** 2
end

# Procedure: produces side effects, returns nil
def log_event(message)
  File.open('events.log', 'a') do |file|
    file.puts "[#{Time.now}] #{message}"
  end
end

area = calculate_area(5.0)
# => 78.53981633974483

log_event('Application started')
# => nil (side effect: writes to file)

The ability to decompose programs into functions and procedures transforms software development from writing monolithic scripts to constructing systems from composable components. This decomposition improves code readability by giving meaningful names to operations, enhances testability by isolating behavior, and facilitates reuse across multiple contexts.

Key Principles

Functions and procedures embody several fundamental principles that govern their design and usage. Understanding these principles clarifies how to structure code for maintainability and correctness.

Abstraction and Encapsulation

Functions hide implementation complexity behind a named interface. Callers need only understand what the function does, not how it accomplishes its task. This separation of interface from implementation allows internal changes without affecting calling code. The function signature defines the contract between caller and implementation.

# Caller sees simple interface
result = parse_json(input_string)

# Implementation details hidden
def parse_json(string)
  # Complex parsing logic
  # Error handling
  # Validation
  JSON.parse(string)
rescue JSON::ParserError => e
  handle_parse_error(e)
end

Input and Output

Functions receive input through parameters and communicate results through return values. Parameters define what information the function needs to perform its task. Return values communicate the result back to the caller. This input-output model creates clear data flow through the program.

Ruby methods receive parameters positionally, by keyword, or through blocks. Methods return the value of the last evaluated expression, or explicitly via the return keyword. The return value becomes the result of the method call expression.

Scope and Lifetime

Variables declared within a function exist only during function execution. Parameters and local variables have function scope, inaccessible outside the function body. This encapsulation prevents unintended interactions between different parts of the program.

def process_data(values)
  total = values.sum        # local variable
  average = total / values.size  # local variable
  average
end

result = process_data([1, 2, 3, 4, 5])
# => 3

# total and average don't exist here
# This would raise NameError:
# puts total

Side Effects and Purity

A pure function produces no observable effects beyond returning a value. Given identical inputs, it always returns identical outputs. Pure functions depend only on their parameters, not on external state. Procedures, by contrast, intentionally produce side effects: modifying variables, writing files, sending network requests, or altering program state.

Pure functions offer significant advantages for testing and reasoning about code. They require no setup or teardown, produce predictable results, and cannot create subtle bugs through unexpected state modifications. However, programs must produce side effects to accomplish useful work. The distinction guides where to isolate effects versus where to keep functions pure.

Composition

Functions compose by passing the output of one function as input to another. This composition builds complex operations from simpler building blocks. The ability to combine functions freely requires that their interfaces match appropriately.

def normalize(text)
  text.downcase.strip
end

def remove_punctuation(text)
  text.gsub(/[[:punct:]]/, '')
end

def word_count(text)
  text.split.length
end

# Composition through chaining
count = word_count(remove_punctuation(normalize(input)))

# Or using method chaining
count = input
  .then { |t| normalize(t) }
  .then { |t| remove_punctuation(t) }
  .then { |t| word_count(t) }

Single Responsibility

Each function should perform one well-defined task. Functions that do multiple unrelated things become difficult to name, test, and reuse. Single responsibility makes functions easier to understand and modify without unintended consequences.

The appropriate level of granularity depends on context. A function might perform "one thing" that involves multiple steps, as long as those steps work toward a single coherent purpose. The test: can the function be described concisely without using "and" or "or"?

Modularity and Reusability

Functions package logic for reuse across multiple call sites. Rather than duplicating code, the program calls the function wherever that logic applies. Changes to the logic require updating only the function definition, not every location where the logic appears.

Reusability improves with functions that make minimal assumptions about their context. Functions that depend on specific global state or operate only on particular data structures have limited applicability. Functions that operate through parameters and return values work in more contexts.

Ruby Implementation

Ruby implements functions as methods defined within classes, modules, or at the top level. The language provides multiple mechanisms for creating callable code: methods, procs, lambdas, and blocks. Each serves different purposes and exhibits distinct behavior.

Method Definition

Methods define reusable code with the def keyword. Methods defined at the top level become private methods of Object, available globally but not accessible via dot notation. Methods defined within classes belong to their class or instance.

# Top-level method
def greet(name)
  "Hello, #{name}!"
end

greet('Alice')
# => "Hello, Alice!"

# Class method
class Calculator
  def self.add(a, b)
    a + b
  end
end

Calculator.add(5, 3)
# => 8

# Instance method
class Person
  def initialize(name)
    @name = name
  end
  
  def introduce
    "My name is #{@name}"
  end
end

person = Person.new('Bob')
person.introduce
# => "My name is Bob"

Parameter Handling

Ruby methods accept parameters in multiple forms: positional, keyword, default values, splat operators, and block parameters. This flexibility supports various calling conventions while maintaining clear signatures.

# Positional parameters
def calculate(x, y)
  x * y
end

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

greet('Alice')           # => "Hello, Alice!"
greet('Alice', 'Hi')     # => "Hi, Alice!"

# Keyword parameters
def create_user(name:, email:, age: nil)
  { name: name, email: email, age: age }
end

create_user(name: 'Alice', email: 'alice@example.com')
# => {:name=>"Alice", :email=>"alice@example.com", :age=>nil}

# Splat operator for variable arguments
def sum(*numbers)
  numbers.reduce(0, :+)
end

sum(1, 2, 3, 4, 5)
# => 15

# Double splat for keyword arguments
def configure(**options)
  options.each { |key, value| puts "#{key}: #{value}" }
end

configure(host: 'localhost', port: 3000)
# host: localhost
# port: 3000

Return Values

Ruby methods return the value of the last evaluated expression. The return keyword explicitly returns early, useful for guard clauses or multiple return points. Methods without explicit return statements return nil when the last expression is an assignment or certain other statements.

def divide(a, b)
  return nil if b.zero?
  a / b
end

divide(10, 2)   # => 5
divide(10, 0)   # => nil

# Implicit return
def square(x)
  x * x  # Last expression becomes return value
end

# Multiple return points
def status_message(code)
  return 'Success' if code == 200
  return 'Not Found' if code == 404
  return 'Server Error' if code >= 500
  'Unknown Status'
end

Procs and Lambdas

Procs and lambdas represent blocks of code stored in variables and passed as objects. They differ in behavior regarding arguments and return statements. Lambdas enforce argument counts strictly and return control to the caller. Procs accept flexible arguments and return from the enclosing method.

# Proc
my_proc = Proc.new { |x| x * 2 }
my_proc.call(5)      # => 10

# Lambda
my_lambda = lambda { |x| x * 2 }
my_lambda.call(5)    # => 10

# Lambda with arrow syntax
my_lambda = ->(x) { x * 2 }
my_lambda.call(5)    # => 10

# Argument handling difference
flexible_proc = Proc.new { |x, y| x }
flexible_proc.call(5)         # => 5 (ignores missing argument)

strict_lambda = lambda { |x, y| x }
# strict_lambda.call(5)       # ArgumentError: wrong number of arguments

# Return behavior difference
def test_return
  my_proc = Proc.new { return "from proc" }
  my_proc.call
  "after proc"
end

test_return   # => "from proc" (returns from method)

def test_lambda_return
  my_lambda = lambda { return "from lambda" }
  my_lambda.call
  "after lambda"
end

test_lambda_return   # => "after lambda" (lambda returns to caller)

Blocks

Blocks represent anonymous code segments passed to methods. Methods receive blocks implicitly and execute them with yield or capture them as explicit parameters with &block. Blocks enable internal iteration and callback patterns.

# Method accepting a block
def repeat(n)
  n.times { yield }
end

repeat(3) { puts "Hello" }
# Hello
# Hello
# Hello

# Block with parameters
def transform_each(array)
  result = []
  array.each do |element|
    result << yield(element)
  end
  result
end

transform_each([1, 2, 3]) { |n| n * 2 }
# => [2, 4, 6]

# Capturing block as parameter
def execute(&block)
  puts "Before"
  block.call
  puts "After"
end

execute { puts "During" }
# Before
# During
# After

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

optional_block { "Block executed" }   # => "Block executed"
optional_block                        # => "No block provided"

Method Objects

Ruby allows treating methods as objects through the method method. This creates a Method object that can be stored, passed, and called like a proc or lambda. Method objects maintain their binding to the original receiver.

class Calculator
  def add(a, b)
    a + b
  end
end

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

add_method.call(5, 3)    # => 8

# Passing method as callable
def apply_operation(a, b, operation)
  operation.call(a, b)
end

apply_operation(10, 5, add_method)   # => 15

Practical Examples

Functions and procedures structure real-world programs by organizing logic into coherent units. These examples demonstrate progressively complex scenarios.

Data Transformation Pipeline

Processing data through multiple transformation steps benefits from composing functions. Each function handles one transformation, making the pipeline clear and testable.

def read_csv(filename)
  File.readlines(filename).map { |line| line.strip.split(',') }
end

def parse_numbers(rows)
  rows.map { |row| row.map(&:to_f) }
end

def calculate_averages(rows)
  rows.map { |row| row.sum / row.size }
end

def format_results(averages)
  averages.map.with_index { |avg, idx| "Row #{idx + 1}: #{avg.round(2)}" }
end

def process_data_file(filename)
  read_csv(filename)
    .then { |rows| parse_numbers(rows) }
    .then { |rows| calculate_averages(rows) }
    .then { |avgs| format_results(avgs) }
end

results = process_data_file('data.csv')
results.each { |result| puts result }
# Row 1: 45.67
# Row 2: 78.23
# Row 3: 56.89

Configuration and Setup

Setup procedures encapsulate initialization logic, handling configuration, validation, and resource allocation. These procedures produce side effects but return objects representing the configured state.

def load_configuration(environment)
  config_file = "config/#{environment}.yml"
  raise "Configuration not found: #{config_file}" unless File.exist?(config_file)
  
  YAML.load_file(config_file)
end

def validate_configuration(config)
  required_keys = %w[database api_key port]
  missing = required_keys - config.keys.map(&:to_s)
  
  raise "Missing configuration keys: #{missing.join(', ')}" if missing.any?
  
  config
end

def initialize_database(config)
  db_config = config['database']
  connection = Database.connect(
    host: db_config['host'],
    port: db_config['port'],
    database: db_config['name']
  )
  
  connection.test_connection
  connection
end

def setup_application(environment)
  config = load_configuration(environment)
  validate_configuration(config)
  
  {
    config: config,
    database: initialize_database(config),
    logger: Logger.new("logs/#{environment}.log")
  }
end

# Usage
app = setup_application('production')
app[:logger].info("Application started")

Business Logic Extraction

Extracting business logic into functions isolates domain rules from infrastructure concerns. This separation clarifies intent and simplifies testing.

class Order
  attr_reader :items, :customer
  
  def initialize(items, customer)
    @items = items
    @customer = customer
  end
  
  def calculate_subtotal
    items.map { |item| item[:price] * item[:quantity] }.sum
  end
  
  def calculate_discount(subtotal)
    return subtotal * 0.2 if customer[:loyalty_tier] == :gold
    return subtotal * 0.1 if customer[:loyalty_tier] == :silver
    return subtotal * 0.15 if subtotal > 100
    0
  end
  
  def calculate_tax(amount)
    amount * customer[:tax_rate]
  end
  
  def calculate_shipping(subtotal)
    return 0 if subtotal > 50
    return 5.99 if customer[:country] == 'US'
    9.99
  end
  
  def calculate_total
    subtotal = calculate_subtotal
    discount = calculate_discount(subtotal)
    discounted = subtotal - discount
    tax = calculate_tax(discounted)
    shipping = calculate_shipping(subtotal)
    
    discounted + tax + shipping
  end
end

order = Order.new(
  [
    { name: 'Widget', price: 25.00, quantity: 2 },
    { name: 'Gadget', price: 15.00, quantity: 3 }
  ],
  { loyalty_tier: :silver, tax_rate: 0.08, country: 'US' }
)

order.calculate_total
# => 99.69 (subtotal: 95.00, discount: 9.50, subtotal after: 85.50, tax: 6.84, shipping: 5.99, total: 99.69)

Error Recovery and Retry Logic

Functions encapsulate error handling and retry strategies, separating failure recovery from core logic. This makes the system more resilient to transient failures.

def retry_with_backoff(max_attempts: 3, initial_delay: 1)
  attempts = 0
  
  begin
    attempts += 1
    yield
  rescue => error
    if attempts < max_attempts
      delay = initial_delay * (2 ** (attempts - 1))
      puts "Attempt #{attempts} failed: #{error.message}. Retrying in #{delay}s..."
      sleep delay
      retry
    else
      raise error
    end
  end
end

def fetch_data_from_api(url)
  response = HTTP.get(url)
  raise "HTTP error: #{response.code}" unless response.code == 200
  JSON.parse(response.body)
end

def fetch_with_retry(url)
  retry_with_backoff(max_attempts: 3, initial_delay: 2) do
    fetch_data_from_api(url)
  end
rescue => error
  puts "All attempts failed: #{error.message}"
  nil
end

data = fetch_with_retry('https://api.example.com/data')

Design Considerations

Designing effective functions requires balancing multiple factors: granularity, coupling, cohesion, and interface clarity. These decisions affect code maintainability and evolution over time.

Function Granularity

Functions should be small enough to understand quickly but large enough to perform a meaningful operation. A function that does too little adds unnecessary indirection. A function that does too much becomes difficult to test and modify.

Indicators that a function is too large: difficulty naming it concisely, multiple levels of abstraction mixed within the same function, testing requiring extensive setup, changes to one part affecting unrelated parts. Extract smaller functions when a portion of logic becomes reusable, when a code block requires a comment to explain its purpose, or when testing requires mocking different aspects independently.

# Too granular
def add(a, b)
  a + b
end

def multiply(a, b)
  a * b
end

def calculate_rectangle_area(width, height)
  multiply(width, height)  # Unnecessary indirection
end

# Appropriate granularity
def calculate_rectangle_area(width, height)
  width * height
end

def calculate_room_paint_needed(width, length, height, coats: 2, coverage_per_gallon: 350)
  wall_area = 2 * (width + length) * height
  total_area = wall_area * coats
  gallons = (total_area / coverage_per_gallon).ceil
  { gallons: gallons, area: wall_area, coverage: total_area }
end

Single Responsibility Principle

Each function should have one reason to change. Functions that mix different concerns create hidden dependencies and make changes risky. A function processing user input, validating data, updating a database, and sending notifications violates this principle.

Separate functions by the aspect they address: data transformation, validation, persistence, communication, coordination. This separation clarifies intent and enables testing each concern independently.

# Multiple responsibilities
def process_user_registration(params)
  # Validation
  return { error: 'Email invalid' } unless params[:email].include?('@')
  return { error: 'Password too short' } unless params[:password].length >= 8
  
  # Database operation
  user = User.create(email: params[:email], password: params[:password])
  
  # External communication
  send_welcome_email(user.email)
  
  # Logging
  log_registration(user)
  
  { success: true, user: user }
end

# Separated responsibilities
def validate_registration(params)
  errors = []
  errors << 'Email invalid' unless params[:email].include?('@')
  errors << 'Password too short' unless params[:password].length >= 8
  errors
end

def create_user(params)
  User.create(email: params[:email], password: params[:password])
end

def complete_registration(params)
  errors = validate_registration(params)
  return { error: errors.join(', ') } if errors.any?
  
  user = create_user(params)
  send_welcome_email(user.email)
  log_registration(user)
  
  { success: true, user: user }
end

Interface Design

Function signatures define contracts between callers and implementations. Well-designed interfaces minimize coupling, make intent clear, and remain stable as implementations evolve.

Prefer passing specific values over entire objects when the function needs only a few fields. This reduces coupling and makes dependencies explicit. However, when many parameters cluster together, a data object can simplify the signature while keeping coupling manageable.

# Coupled to entire object
def format_address(user)
  "#{user.street}, #{user.city}, #{user.state} #{user.zip}"
end

# Decoupled, explicit dependencies
def format_address(street:, city:, state:, zip:)
  "#{street}, #{city}, #{state} #{zip}"
end

# When many parameters cluster, use data object
AddressData = Struct.new(:street, :city, :state, :zip, keyword_init: true)

def format_address(address)
  "#{address.street}, #{address.city}, #{address.state} #{address.zip}"
end

Side Effects and Pure Functions

Pure functions simplify reasoning about code by guaranteeing consistent behavior regardless of when or how often they execute. They enable parallel execution, memoization, and fearless refactoring. Isolate side effects to specific functions, keeping the majority of code pure.

When side effects are necessary, make them obvious through naming and documentation. Functions named save, update, send, or delete signal side effects. Functions named calculate, parse, find, or format suggest purity.

# Pure function
def calculate_discount(price, rate)
  price * rate
end

# Procedure with side effects (name signals this)
def apply_discount!(order, rate)
  discount = calculate_discount(order.total, rate)
  order.discount = discount
  order.save
end

# Keeping logic pure, side effects explicit
def determine_order_status(order)
  return :shipped if order.shipped_at
  return :processing if order.processed_at
  :pending
end

def update_order_status!(order)
  status = determine_order_status(order)
  order.update(status: status)
end

Return Value Design

Functions communicate results through return values. Consistent return types simplify error handling and composition. Functions that sometimes return a value and sometimes return nil require defensive checks throughout calling code.

Consider returning result objects that encapsulate success or failure, carrying either the result or error information. This makes error handling explicit and compositional.

# Inconsistent returns
def find_user(id)
  user = User.find_by(id: id)
  return nil unless user
  return nil unless user.active?
  user
end

# Consistent returns with explicit success/failure
Result = Struct.new(:success?, :value, :error, keyword_init: true)

def find_user(id)
  user = User.find_by(id: id)
  return Result.new(success?: false, error: 'User not found') unless user
  return Result.new(success?: false, error: 'User inactive') unless user.active?
  
  Result.new(success?: true, value: user)
end

# Usage
result = find_user(123)
if result.success?
  process_user(result.value)
else
  handle_error(result.error)
end

Common Patterns

Functions follow recurring patterns that solve common problems. Understanding these patterns provides a vocabulary for design discussions and recognizes opportunities for their application.

Parameter Object Pattern

When functions accept many parameters, especially when those parameters frequently appear together, a parameter object groups related data. This reduces function signatures, makes relationships explicit, and simplifies adding new parameters.

# Many individual parameters
def create_invoice(customer_name, customer_email, items, tax_rate, discount_rate, currency)
  # ...
end

# Parameter object
InvoiceParams = Struct.new(
  :customer_name, 
  :customer_email, 
  :items, 
  :tax_rate, 
  :discount_rate, 
  :currency,
  keyword_init: true
)

def create_invoice(params)
  # Access via params.customer_name, params.items, etc.
end

params = InvoiceParams.new(
  customer_name: 'Alice',
  customer_email: 'alice@example.com',
  items: items,
  tax_rate: 0.08,
  discount_rate: 0.1,
  currency: 'USD'
)
create_invoice(params)

Higher-Order Functions

Functions that accept other functions as parameters or return functions enable powerful abstractions. They extract control flow patterns, allowing behavior to be parameterized through passed functions.

# Function accepting function as parameter
def filter(collection, predicate)
  result = []
  collection.each do |item|
    result << item if predicate.call(item)
  end
  result
end

numbers = [1, 2, 3, 4, 5, 6]
evens = filter(numbers, ->(n) { n.even? })
# => [2, 4, 6]

# Function returning function
def multiply_by(factor)
  ->(x) { x * factor }
end

double = multiply_by(2)
triple = multiply_by(3)

double.call(5)   # => 10
triple.call(5)   # => 15

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

add_one = ->(x) { x + 1 }
square = ->(x) { x * x }

square_then_add = compose(add_one, square)
square_then_add.call(3)   # => 10 (3^2 + 1)

Callback Pattern

Callbacks pass functions that execute at specific points during another function's execution. This decouples the timing of operations from their implementation, enabling hooks for customization and notification.

def process_items(items, on_each: nil, on_complete: nil)
  results = []
  
  items.each do |item|
    result = yield(item)  # Main processing
    results << result
    on_each&.call(item, result)  # Optional callback per item
  end
  
  on_complete&.call(results) if on_complete  # Optional completion callback
  results
end

# Usage with callbacks
processed = process_items(
  [1, 2, 3, 4, 5],
  on_each: ->(item, result) { puts "Processed #{item} -> #{result}" },
  on_complete: ->(results) { puts "Total: #{results.sum}" }
) { |n| n * 2 }

# Processed 1 -> 2
# Processed 2 -> 4
# Processed 3 -> 6
# Processed 4 -> 8
# Processed 5 -> 10
# Total: 30

Template Method Pattern

Template methods define the skeleton of an algorithm, delegating specific steps to helper functions. This separates invariant structure from variant behavior, allowing subclasses or configuration to customize steps while maintaining overall flow.

def process_payment(payment, validator:, processor:, notifier:)
  # Template structure
  return { error: 'Validation failed' } unless validator.call(payment)
  
  result = processor.call(payment)
  return { error: result[:error] } if result[:error]
  
  notifier.call(payment, result)
  
  { success: true, result: result }
end

# Different validators
basic_validator = ->(payment) { payment[:amount] > 0 }
strict_validator = ->(payment) { 
  payment[:amount] > 0 && payment[:card_number].length == 16
}

# Different processors
test_processor = ->(payment) { { transaction_id: 'test_123' } }
real_processor = ->(payment) { 
  # Actual payment gateway integration
  { transaction_id: SecureRandom.uuid }
}

# Different notifiers
email_notifier = ->(payment, result) { 
  send_email(payment[:email], "Payment processed: #{result[:transaction_id]}")
}
log_notifier = ->(payment, result) {
  logger.info("Payment #{result[:transaction_id]} processed")
}

# Use with different combinations
process_payment(
  payment,
  validator: strict_validator,
  processor: real_processor,
  notifier: email_notifier
)

Guard Clause Pattern

Guard clauses handle exceptional conditions early, returning immediately rather than nesting the main logic in conditional blocks. This keeps the happy path at the lowest indentation level, improving readability.

# Nested conditionals
def process_order(order)
  if order
    if order.valid?
      if order.items.any?
        if order.customer.active?
          # Main logic deeply nested
          calculate_total(order)
        end
      end
    end
  end
end

# Guard clauses
def process_order(order)
  return nil unless order
  return nil unless order.valid?
  return nil if order.items.empty?
  return nil unless order.customer.active?
  
  # Main logic at base indentation
  calculate_total(order)
end

Method Chaining Pattern

Methods that return self enable chaining multiple operations in a fluent interface. This creates readable sequences of transformations while maintaining immutability when desired.

class QueryBuilder
  def initialize(table)
    @table = table
    @conditions = []
    @order = nil
    @limit = nil
  end
  
  def where(condition)
    @conditions << condition
    self  # Return self to enable chaining
  end
  
  def order_by(field)
    @order = field
    self
  end
  
  def limit(n)
    @limit = n
    self
  end
  
  def to_sql
    sql = "SELECT * FROM #{@table}"
    sql += " WHERE #{@conditions.join(' AND ')}" if @conditions.any?
    sql += " ORDER BY #{@order}" if @order
    sql += " LIMIT #{@limit}" if @limit
    sql
  end
end

query = QueryBuilder.new('users')
  .where('age > 18')
  .where('active = true')
  .order_by('name')
  .limit(10)
  .to_sql

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

Testing Approaches

Functions designed for testability exhibit specific characteristics: deterministic behavior, explicit dependencies, and clear inputs and outputs. Testing strategies verify correctness across typical cases, edge cases, and error conditions.

Unit Testing Pure Functions

Pure functions offer the simplest testing scenario. Tests provide inputs and assert outputs without setup or teardown. The absence of side effects means tests run independently and deterministically.

require 'minitest/autorun'

class MathFunctions
  def self.factorial(n)
    return 1 if n <= 1
    n * factorial(n - 1)
  end
  
  def self.fibonacci(n)
    return n if n <= 1
    fibonacci(n - 1) + fibonacci(n - 2)
  end
end

class TestMathFunctions < Minitest::Test
  def test_factorial_base_cases
    assert_equal 1, MathFunctions.factorial(0)
    assert_equal 1, MathFunctions.factorial(1)
  end
  
  def test_factorial_recursive_cases
    assert_equal 2, MathFunctions.factorial(2)
    assert_equal 6, MathFunctions.factorial(3)
    assert_equal 120, MathFunctions.factorial(5)
  end
  
  def test_fibonacci_sequence
    expected = [0, 1, 1, 2, 3, 5, 8, 13]
    expected.each_with_index do |value, index|
      assert_equal value, MathFunctions.fibonacci(index)
    end
  end
end

Testing Functions with Side Effects

Functions that produce side effects require verification that the intended effects occurred. Tests may check file system changes, database modifications, or network calls. Mocking isolates the function under test from external dependencies.

require 'minitest/autorun'
require 'minitest/mock'

class UserRepository
  def save(user)
    Database.execute(
      "INSERT INTO users (name, email) VALUES (?, ?)",
      [user[:name], user[:email]]
    )
  end
  
  def find_by_email(email)
    Database.query(
      "SELECT * FROM users WHERE email = ?",
      [email]
    ).first
  end
end

class TestUserRepository < Minitest::Test
  def test_save_executes_insert
    repo = UserRepository.new
    user = { name: 'Alice', email: 'alice@example.com' }
    
    Database.stub :execute, nil do
      # Verify Database.execute was called with correct arguments
      mock = Minitest::Mock.new
      mock.expect :execute, nil, [
        "INSERT INTO users (name, email) VALUES (?, ?)",
        ['Alice', 'alice@example.com']
      ]
      
      Database.stub :execute, mock do
        repo.save(user)
      end
      
      mock.verify
    end
  end
  
  def test_find_by_email_queries_database
    repo = UserRepository.new
    expected_user = { id: 1, name: 'Alice', email: 'alice@example.com' }
    
    Database.stub :query, [expected_user] do
      result = repo.find_by_email('alice@example.com')
      assert_equal expected_user, result
    end
  end
end

Testing Error Conditions

Tests verify that functions handle errors appropriately: raising exceptions for invalid inputs, returning error values for expected failure conditions, or recovering gracefully from transient failures.

require 'minitest/autorun'

class DataParser
  def self.parse_integer(string)
    raise ArgumentError, 'Input cannot be nil' if string.nil?
    raise ArgumentError, 'Input must be a string' unless string.is_a?(String)
    
    Integer(string)
  rescue ArgumentError => e
    raise ArgumentError, "Cannot parse '#{string}' as integer: #{e.message}"
  end
end

class TestDataParser < Minitest::Test
  def test_parse_valid_integer
    assert_equal 42, DataParser.parse_integer('42')
    assert_equal -10, DataParser.parse_integer('-10')
  end
  
  def test_parse_nil_raises_error
    error = assert_raises(ArgumentError) do
      DataParser.parse_integer(nil)
    end
    assert_match /cannot be nil/, error.message
  end
  
  def test_parse_non_string_raises_error
    error = assert_raises(ArgumentError) do
      DataParser.parse_integer(42)
    end
    assert_match /must be a string/, error.message
  end
  
  def test_parse_invalid_string_raises_error
    error = assert_raises(ArgumentError) do
      DataParser.parse_integer('not a number')
    end
    assert_match /Cannot parse/, error.message
  end
end

Testing Higher-Order Functions

Functions that accept or return other functions require testing both the higher-order function's behavior and the interaction with passed functions.

require 'minitest/autorun'

def map_with_index(collection, &block)
  result = []
  collection.each_with_index do |item, index|
    result << block.call(item, index)
  end
  result
end

class TestHigherOrderFunctions < Minitest::Test
  def test_map_with_index_applies_function
    input = ['a', 'b', 'c']
    result = map_with_index(input) { |item, index| "#{index}:#{item}" }
    
    assert_equal ['0:a', '1:b', '2:c'], result
  end
  
  def test_map_with_index_passes_correct_indices
    indices = []
    map_with_index([10, 20, 30]) { |item, index| indices << index }
    
    assert_equal [0, 1, 2], indices
  end
  
  def test_map_with_index_empty_collection
    result = map_with_index([]) { |item, index| item }
    assert_empty result
  end
end

Reference

Method Definition Syntax

Syntax Description Example
def name Define method without parameters def greet; end
def name(param) Define method with positional parameter def greet(name); end
def name(param1, param2) Multiple positional parameters def add(x, y); end
def name(param = value) Parameter with default value def greet(name = 'World'); end
def name(param:) Required keyword parameter def create(name:); end
def name(param: value) Optional keyword parameter with default def create(name: 'Untitled'); end
def name(*params) Variable positional parameters def sum(*numbers); end
def name(**params) Variable keyword parameters def configure(**options); end
def name(&block) Explicit block parameter def execute(&block); end

Parameter Patterns

Pattern Description Usage
Positional required Parameters must be provided in order def calculate(x, y, z)
Positional with defaults Parameters optional with default values def greet(name, greeting = 'Hello')
Keyword required Parameters by name, must be provided def create(name:, email:)
Keyword optional Parameters by name with defaults def create(name:, role: 'user')
Splat operator Collects remaining positional arguments def sum(*numbers)
Double splat Collects remaining keyword arguments def configure(**options)
Mixed parameters Combination of parameter types def process(id, name: nil, *tags, **meta)

Return Value Patterns

Pattern Description Example
Implicit return Last expression becomes return value def square(x); x * x; end
Explicit return Early return with return keyword return nil if invalid?
Multiple returns Different return points return :success if ok; :failure
Nil return Methods that produce only side effects def save; Database.write; end
Result object Explicit success/failure indication Result.new(success: true, value: data)

Block and Proc Comparison

Feature Block Proc Lambda
Creation Passed to method Proc.new { } lambda { } or ->() { }
Storage Cannot be stored in variable Can be stored Can be stored
Multiple per method Only one implicit Multiple as parameters Multiple as parameters
Return behavior Returns from enclosing method Returns from enclosing method Returns to caller
Argument checking Flexible Flexible Strict
Explicit calls Invoked with yield call method call method

Common Method Signatures

Signature Purpose Example
query methods Return information without side effects def total; @items.sum; end
command methods Perform action with side effects def save!; persist; end
predicate methods Return boolean with ? suffix def valid?; check_validity; end
destructive methods Modify receiver with ! suffix def sort!; @items.sort!; end
factory methods Create new instances def self.build(params); new(params); end
conversion methods Convert to different type def to_s; stringify; end

Scope Rules

Scope Type Visibility Example
Local variables Current method only x = 10
Instance variables Current object @name = value
Class variables All instances of class @@count = 0
Global variables Entire program $debug = true
Constants Current class and nested classes MAX_SIZE = 100
Parameters Current method def process(value)

Method Visibility

Visibility Description Usage
public Callable from anywhere Default for instance methods
private Callable only within class private def internal; end
protected Callable within class and subclasses protected def helper; end
module_function Module method available as both instance and module method module_function :utility