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