CrackedRuby logo

CrackedRuby

Default Parameter Values

Overview

Default parameter values allow Ruby methods to specify fallback values for parameters when arguments are not provided during method calls. Ruby evaluates default parameter expressions at method call time, not at method definition time, enabling dynamic defaults based on runtime conditions.

Ruby supports default parameters in method definitions, block parameters, lambda parameters, and proc parameters. Default values can be literals, expressions, method calls, or references to other parameters defined earlier in the parameter list.

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

greet              # => "Hello, World!"
greet("Ruby")      # => "Hello, Ruby!"
greet("Ruby", "?") # => "Hello, Ruby?"

The parameter evaluation follows left-to-right order, allowing later parameters to reference earlier ones. Ruby distinguishes between parameters with defaults and those without, affecting method arity and argument validation.

def process_data(data, format = :json, timestamp = Time.now)
  {
    content: data,
    format: format,
    processed_at: timestamp
  }
end

result = process_data("sample data")
# => {:content=>"sample data", :format=>:json, :processed_at=>2024-01-15 10:30:45 UTC}

Default parameters integrate with Ruby's argument handling mechanisms, including splat operators, keyword arguments, and block parameters. The method signature determines which parameters receive default values and how Ruby matches provided arguments to parameters.

class Logger
  def log(message, level = :info, **options)
    output = "[#{level.upcase}] #{message}"
    output += " (#{options})" unless options.empty?
    puts output
  end
end

logger = Logger.new
logger.log("System started")
# => [INFO] System started
logger.log("Database error", :error, source: "db", retry: true)
# => [ERROR] Database error ({:source=>"db", :retry=>true})

Basic Usage

Method parameters with default values use the assignment operator in the parameter list. Ruby assigns the default value when the caller provides fewer arguments than the total parameter count.

def create_user(username, role = "user", active = true)
  {
    username: username,
    role: role,
    active: active,
    created_at: Time.now
  }
end

admin = create_user("admin", "administrator")
user = create_user("john_doe")
inactive_user = create_user("temp_user", "guest", false)

Default parameter expressions execute at method call time, not definition time. This behavior enables dynamic defaults that change based on runtime conditions or method call context.

def generate_filename(base, extension = "txt", timestamp = Time.now.to_i)
  "#{base}_#{timestamp}.#{extension}"
end

# Each call generates a different timestamp
generate_filename("data")        # => "data_1642682445.txt"
sleep(1)
generate_filename("data")        # => "data_1642682446.txt"

Parameters with defaults can reference previously defined parameters, creating dependent default values. Ruby evaluates parameters from left to right, making earlier parameters available to later default expressions.

def create_backup(source_path, backup_dir = File.dirname(source_path), 
                  backup_name = File.basename(source_path) + ".bak")
  backup_path = File.join(backup_dir, backup_name)
  FileUtils.cp(source_path, backup_path)
  backup_path
end

create_backup("/home/user/document.txt")
# Creates backup at "/home/user/document.txt.bak"

Block parameters also support default values, providing fallback behavior when block arguments are not provided or are nil.

def process_items(items, &block)
  block ||= ->(item, index = 0) { puts "Processing #{item} at position #{index}" }
  items.each_with_index(&block)
end

process_items(["a", "b", "c"])
# Output: Processing a at position 0
#         Processing b at position 1
#         Processing c at position 2

Lambda and proc definitions accept default parameters with identical syntax to method definitions. The default evaluation timing remains consistent across all callable objects.

calculate_tax = ->(amount, rate = 0.08, precision = 2) do
  tax = amount * rate
  tax.round(precision)
end

calculate_tax.call(100)        # => 8.0
calculate_tax.call(100, 0.10)  # => 10.0
calculate_tax.call(100, 0.095, 3) # => 9.5

Advanced Usage

Default parameter expressions can invoke methods, access instance variables, or perform complex computations. Ruby evaluates these expressions in the method's binding context, providing access to the receiver's state and methods.

class DatabaseConnection
  def initialize(host = "localhost", port = default_port, timeout = calculate_timeout)
    @host = host
    @port = port
    @timeout = timeout
  end

  private

  def default_port
    ENV["DB_PORT"]&.to_i || 5432
  end

  def calculate_timeout
    base_timeout = 30
    base_timeout *= 2 if production_environment?
    base_timeout
  end

  def production_environment?
    ENV["RAILS_ENV"] == "production"
  end
end

Default parameters work with method overloading patterns, allowing multiple method signatures with different default behavior. Ruby's argument matching selects the appropriate parameter assignments based on provided argument count.

class HttpClient
  def request(method, url, body = nil, headers = default_headers, timeout = 30)
    # Method implementation
  end

  def get(url, headers = default_headers, timeout = 30)
    request(:get, url, nil, headers, timeout)
  end

  def post(url, body, headers = default_headers, timeout = 30)
    request(:post, url, body, headers, timeout)
  end

  private

  def default_headers
    {
      "Content-Type" => "application/json",
      "User-Agent" => "Ruby/#{RUBY_VERSION}"
    }
  end
end

Complex default expressions can build data structures, perform calculations, or integrate with external systems. These expressions execute within the method's security and performance context.

class ReportGenerator
  def generate_report(data, 
                      format = detect_format_from_data(data),
                      output_path = generate_output_path(format),
                      config = build_default_config(data.size))
    
    processor = create_processor(format, config)
    result = processor.process(data)
    File.write(output_path, result)
    output_path
  end

  private

  def detect_format_from_data(data)
    case data
    when Array then :csv
    when Hash then :json
    else :text
    end
  end

  def generate_output_path(format)
    timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
    "/tmp/report_#{timestamp}.#{format}"
  end

  def build_default_config(data_size)
    {
      compression: data_size > 1000,
      parallel_processing: data_size > 10000,
      memory_limit: calculate_memory_limit(data_size)
    }
  end

  def calculate_memory_limit(size)
    base_limit = 100 * 1024 * 1024  # 100MB
    size > 50000 ? base_limit * 4 : base_limit
  end
end

Default parameters interact with Ruby's splat operators and keyword arguments, creating flexible method signatures that handle variable argument lists while maintaining default behavior.

class EventEmitter
  def emit(event_name, *args, timestamp: Time.now, metadata: {}, **options)
    event_data = {
      name: event_name,
      args: args,
      timestamp: timestamp,
      metadata: metadata.merge(options)
    }
    
    notify_listeners(event_data)
  end

  def on(event_name, priority = 0, &handler)
    @listeners ||= {}
    @listeners[event_name] ||= []
    @listeners[event_name] << { priority: priority, handler: handler }
    @listeners[event_name].sort_by! { |l| -l[:priority] }
  end

  private

  def notify_listeners(event_data)
    listeners = @listeners&.[](event_data[:name]) || []
    listeners.each { |listener| listener[:handler].call(event_data) }
  end
end

emitter = EventEmitter.new
emitter.on("user_login") { |event| puts "Login: #{event[:args].first}" }
emitter.on("user_login", 10) { |event| puts "Priority handler: #{event[:timestamp]}" }
emitter.emit("user_login", "john_doe", ip_address: "192.168.1.1")

Metaprogramming techniques can generate methods with dynamic default parameters, creating flexible APIs that adapt to runtime conditions or configuration.

class ConfigurableService
  def self.define_endpoint(name, default_method: :get, default_timeout: 30)
    define_method(name) do |url, method = default_method, timeout = default_timeout, **options|
      request_config = {
        method: method,
        url: url,
        timeout: timeout,
        headers: options[:headers] || {},
        params: options[:params] || {}
      }
      
      make_request(request_config)
    end
  end

  define_endpoint :fetch_users, default_timeout: 10
  define_endpoint :create_user, default_method: :post, default_timeout: 45
  define_endpoint :upload_file, default_method: :put, default_timeout: 120
end

Common Pitfalls

Default parameter expressions share mutable objects across method calls, creating unexpected behavior when the default value is modified. Ruby evaluates the expression each time but may return the same object reference.

# Problematic: mutable default
def add_item(item, collection = [])
  collection << item
  collection
end

list1 = add_item("apple")     # => ["apple"]
list2 = add_item("banana")    # => ["apple", "banana"] - unexpected!

# Correct: create new instance each time
def add_item(item, collection = nil)
  collection ||= []
  collection << item
  collection
end

The same issue occurs with Hash, String, and other mutable objects used as default values. Each method call receives the same object instance unless the default expression creates a new object.

# Problematic: shared hash modification
def track_event(name, metadata = {})
  metadata[:timestamp] = Time.now
  metadata[:event] = name
  log_event(metadata)
end

track_event("login")           # Works as expected
track_event("logout")          # Contains both login and logout data

# Correct: ensure new hash creation
def track_event(name, metadata = nil)
  metadata = {} if metadata.nil?
  metadata = metadata.dup  # Create copy if hash provided
  metadata[:timestamp] = Time.now
  metadata[:event] = name
  log_event(metadata)
end

Default parameter evaluation occurs in method definition order, but Ruby raises errors when parameters reference later parameters that haven't been assigned yet.

# Error: later parameter referenced before definition
def invalid_order(second = first * 2, first = 10)
  [first, second]
end

# Correct: parameters reference only earlier parameters  
def valid_order(first = 10, second = first * 2)
  [first, second]
end

Performance issues arise when default parameter expressions perform expensive operations, especially when the default value isn't used. Ruby evaluates all default expressions during method binding, regardless of argument provision.

# Problematic: expensive default always evaluated
def process_data(data, config = load_expensive_config_from_database)
  # Even when config is provided, load_expensive_config_from_database runs
  data.transform(&config[:processor])
end

# Better: lazy evaluation pattern
def process_data(data, config = nil)
  config ||= load_expensive_config_from_database
  data.transform(&config[:processor])
end

Method arity calculations become complex with default parameters, affecting dynamic method calls and argument validation. Ruby's method.arity returns negative values for methods with optional parameters.

def method_with_defaults(required, optional = "default", *rest)
  [required, optional, rest]
end

method = method(:method_with_defaults)
method.arity  # => -2 (negative indicates optional parameters)

# This affects dynamic calling
def call_with_args(method_name, *args)
  method = method(method_name)
  if method.arity >= 0
    # Fixed arity method
    send(method_name, *args.first(method.arity))
  else
    # Variable arity method with defaults
    send(method_name, *args)
  end
end

Default parameters in blocks and procs create subtle scoping issues when default expressions reference local variables from the surrounding context.

multiplier = 5

# The default captures multiplier at block creation time
processor = ->(value, factor = multiplier) { value * factor }

multiplier = 10  # This change doesn't affect the default

processor.call(3)     # => 15 (uses original multiplier value)
processor.call(3, 2)  # => 6  (uses provided factor)

Exception handling during default parameter evaluation can mask errors or create confusing stack traces. Exceptions in default expressions propagate immediately, potentially hiding the actual method logic issues.

def risky_defaults(data, processor = dangerous_default_processor)
  processor.call(data)
end

def dangerous_default_processor
  raise "Default processor failed"
end

# Error occurs before method body executes
begin
  risky_defaults("test", safe_processor)  # Still fails due to default evaluation
rescue => e
  puts e.message  # => "Default processor failed"
end

Reference

Method Definition Syntax

Pattern Example Description
def method(param = value) def greet(name = "World") Basic default parameter
def method(p1, p2 = value) def log(msg, level = :info) Mixed required and default
def method(p1 = v1, p2 = v2) def connect(host = "localhost", port = 3000) Multiple defaults
def method(req, opt = val, *rest) def process(data, format = :json, *args) Default with splat
def method(req, opt = val, **opts) def call(url, timeout = 30, **headers) Default with keyword splat

Parameter Reference Rules

Reference Type Valid Example
Literal values def method(x = 42, y = "text")
Earlier parameters def method(x = 10, y = x * 2)
Later parameters def method(x = y, y = 10) - Error
Instance methods def method(x = helper_method)
Instance variables def method(x = @default_value)
Class methods def method(x = self.class.default)
Local variables def method(x = local_var)

Arity Behavior

Method Signature method.arity Description
def m(a) 1 One required parameter
def m(a = 1) -1 Zero or one parameter
def m(a, b = 1) -2 One or two parameters
def m(a = 1, b = 2) -1 Zero, one, or two parameters
def m(a, b = 1, *c) -2 One or more parameters
def m(a = 1, **b) -1 Zero or more parameters

Default Expression Evaluation

Expression Type Evaluation Time Shared State
Literal values Method call Immutable objects shared
Method calls Method call New result each call
Mutable objects Method call Same object reference
Calculations Method call New result each call
Time.now Method call Different each call
[] or {} Method call Same object reference

Common Default Patterns

# Safe mutable defaults
def method(array = nil)
  array ||= []
end

# Configuration defaults
def method(options = {})
  options = DEFAULT_OPTIONS.merge(options)
end

# Timestamp defaults
def method(timestamp = Time.now)
  # Creates new timestamp each call
end

# Calculated defaults
def method(size = calculate_optimal_size)
  # Calculation runs each call
end

# Conditional defaults
def method(value = production? ? 100 : 10)
  # Environment-based defaults
end

Error Conditions

Error Type Cause Example
NameError Reference undefined variable def m(x = undefined_var)
ArgumentError Forward reference to parameter def m(x = y, y = 1)
NoMethodError Call undefined method def m(x = undefined_method)
TypeError Invalid default expression def m(x = 1 + "text")

Memory and Performance

Pattern Memory Impact Performance Impact
Literal defaults Minimal Minimal
Method call defaults Variable Method call overhead
Object creation defaults High if large objects Object allocation cost
Database/IO defaults High Network/disk latency
Complex calculations Variable Computation overhead

Block and Proc Defaults

# Lambda with defaults
lambda_with_defaults = ->(x, y = 10) { x + y }

# Proc with defaults  
proc_with_defaults = proc { |x, y = 5| x * y }

# Block parameter defaults
def method(&block)
  block ||= ->(item, index = 0) { puts "#{index}: #{item}" }
end