Overview
Ruby implements variable arguments through the splat operator (*) applied to a parameter name, conventionally *args
. When a method declares *args
, Ruby collects all positional arguments that don't match other parameters into an array. This mechanism enables methods to handle flexible argument lists without knowing the exact number of arguments in advance.
The *args
parameter captures arguments after required parameters and before keyword arguments. Ruby converts the collected arguments into a standard Array object, making all array methods available for processing the arguments within the method body.
def process_items(*items)
items.each { |item| puts "Processing: #{item}" }
end
process_items("file1.txt", "file2.txt", "file3.txt")
# Processing: file1.txt
# Processing: file2.txt
# Processing: file3.txt
Ruby processes arguments in a specific order: required parameters first, then optional parameters with defaults, followed by *args
, and finally keyword arguments. When calling methods with *args
, Ruby performs argument assignment before collecting remaining arguments into the array.
def mixed_params(required, optional = "default", *extras, keyword:)
puts "Required: #{required}"
puts "Optional: #{optional}"
puts "Extras: #{extras.inspect}"
puts "Keyword: #{keyword}"
end
mixed_params("req", "opt", "extra1", "extra2", keyword: "kw")
# Required: req
# Optional: opt
# Extras: ["extra1", "extra2"]
# Keyword: kw
The *args
mechanism supports method delegation patterns, constructor overloading, and wrapper methods. Ruby creates a new Array object for each method call, even when no extra arguments are passed, resulting in an empty array rather than nil
.
Basic Usage
Methods with *args
accept zero or more additional arguments beyond any required parameters. Ruby assigns required parameters first, then collects remaining positional arguments into the *args
array.
def log_message(level, *messages)
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
puts "[#{timestamp}] #{level.upcase}"
messages.each { |msg| puts " #{msg}" }
end
log_message("info", "User logged in")
log_message("error", "Database connection failed", "Retrying in 5 seconds")
When no additional arguments are provided, *args
becomes an empty array. This behavior ensures consistent method interfaces and eliminates the need for nil
checks when processing the arguments.
def summarize(title, *details)
puts "Summary: #{title}"
if details.empty?
puts "No additional details provided"
else
details.each_with_index { |detail, i| puts "#{i + 1}. #{detail}" }
end
end
summarize("Project Status")
# Summary: Project Status
# No additional details provided
summarize("Project Status", "Phase 1 complete", "Phase 2 in progress")
# Summary: Project Status
# 1. Phase 1 complete
# 2. Phase 2 in progress
The splat operator works in reverse when calling methods. Prefixing an array with *
expands its elements as separate arguments to the method call. This enables dynamic method calls with programmatically determined argument lists.
def calculate_average(*numbers)
return 0 if numbers.empty?
numbers.sum.to_f / numbers.length
end
scores = [85, 92, 78, 96, 88]
average = calculate_average(*scores)
puts "Average: #{average}" # Average: 87.8
Ruby combines *args
with block parameters naturally. Methods can accept variable arguments and yield to blocks using the collected arguments or processed results.
def batch_process(*items, &block)
return items unless block_given?
items.map(&block)
end
numbers = batch_process(1, 2, 3, 4, 5) { |n| n * n }
puts numbers.inspect # [1, 4, 9, 16, 25]
Advanced Usage
Ruby allows complex parameter combinations mixing required parameters, optional parameters with defaults, *args
, and keyword arguments. Understanding argument precedence prevents unexpected behavior when designing flexible APIs.
class DataProcessor
def initialize(source, target = nil, *filters, format: :json, **options)
@source = source
@target = target || "#{source}.processed"
@filters = filters
@format = format
@options = options
end
def process
puts "Processing #{@source} -> #{@target}"
puts "Filters: #{@filters.join(', ')}" unless @filters.empty?
puts "Format: #{@format}"
puts "Options: #{@options}" unless @options.empty?
end
end
processor = DataProcessor.new(
"data.csv",
"clean_data.json",
"remove_nulls",
"normalize_names",
format: :json,
encoding: "utf-8",
validate: true
)
processor.process
Method delegation patterns benefit from *args
combined with keyword argument forwarding. Ruby 2.7+ introduced argument forwarding syntax (...
) for cleaner delegation.
class LoggingWrapper
def initialize(target)
@target = target
end
# Ruby 2.7+ forwarding syntax
def method_missing(name, ...)
puts "Calling #{name} on #{@target.class}"
result = @target.send(name, ...)
puts "Result: #{result.inspect}"
result
end
# Pre-2.7 equivalent
def legacy_delegate(name, *args, **kwargs, &block)
puts "Calling #{name} on #{@target.class}"
result = @target.send(name, *args, **kwargs, &block)
puts "Result: #{result.inspect}"
result
end
end
Recursive methods often use *args
to handle variable-depth operations. The splat operator enables clean recursive calls with modified argument lists.
def deep_merge(*hashes)
return {} if hashes.empty?
return hashes.first.dup if hashes.length == 1
result = {}
hashes.each do |hash|
hash.each do |key, value|
if result[key].is_a?(Hash) && value.is_a?(Hash)
result[key] = deep_merge(result[key], value)
else
result[key] = value
end
end
end
result
end
config = deep_merge(
{ database: { host: "localhost", port: 5432 } },
{ database: { name: "myapp", timeout: 30 } },
{ cache: { enabled: true, ttl: 300 } }
)
Metaprogramming scenarios frequently employ *args
for dynamic method definition. The flexibility of variable arguments supports code generation and method factories.
class MethodFactory
def self.create_accessor_methods(*attr_names, prefix: nil, suffix: nil)
attr_names.each do |name|
method_name = [prefix, name, suffix].compact.join("_")
define_method(method_name) do
instance_variable_get("@#{name}")
end
define_method("#{method_name}=") do |value|
instance_variable_set("@#{name}", value)
end
end
end
end
class User
extend MethodFactory
create_accessor_methods(:email, :phone, prefix: "contact")
def initialize(email, phone)
@email = email
@phone = phone
end
end
user = User.new("test@example.com", "555-1234")
puts user.contact_email # test@example.com
puts user.contact_phone # 555-1234
Common Pitfalls
Argument order confusion represents the most frequent mistake with *args
. Ruby processes parameters in strict order, and misunderstanding this sequence leads to arguments appearing in unexpected positions within the *args
array.
# Problematic: optional parameter after *args
def broken_method(*items, count = 10)
# This doesn't work as expected
items.first(count)
end
# Correct: optional parameter before *args
def working_method(count = 10, *items)
items.first(count)
end
# Better: use keyword arguments for optional parameters
def best_method(*items, count: 10)
items.first(count)
end
Performance degradation occurs when methods create large argument arrays unnecessarily. Each *args
call allocates a new Array object, and methods processing many arguments repeatedly may experience memory pressure.
# Inefficient: creates arrays for single-item processing
def process_slowly(*items)
items.each do |item|
# Array creation overhead for each call
expensive_operation(item)
end
end
# Better: handle single items directly
def process_efficiently(*items)
case items.length
when 0
return
when 1
expensive_operation(items.first)
else
items.each { |item| expensive_operation(item) }
end
end
Splat operator misuse in method calls causes argument explosion when developers forget that arrays need explicit splatting. This mistake results in methods receiving arrays instead of individual arguments.
def calculate_sum(*numbers)
numbers.sum
end
values = [10, 20, 30]
# Wrong: passes array as single argument
result1 = calculate_sum(values) # NoMethodError: sum not defined for Array
# Correct: splats array into individual arguments
result2 = calculate_sum(*values) # 60
Keyword argument interaction creates subtle bugs when *args
methods also accept keyword arguments. Ruby's argument parsing can produce unexpected results when mixing positional and keyword arguments.
def confusing_method(*args, type: "default")
puts "Args: #{args.inspect}"
puts "Type: #{type}"
end
# This works as expected
confusing_method(1, 2, 3, type: "custom")
# Args: [1, 2, 3]
# Type: custom
# This might surprise you in older Ruby versions
confusing_method(1, 2, { type: "hash" })
# Args: [1, 2, {:type=>"hash"}] # Hash becomes positional argument
# Type: default
Method signature evolution problems arise when adding required parameters to methods that previously used only *args
. This change breaks existing code that relied on the original flexible interface.
# Original method
def log_events(*events)
events.each { |event| puts event }
end
# Breaking change: adding required parameter
def log_events(level, *events) # Breaks existing calls
events.each { |event| puts "[#{level}] #{event}" }
end
# Better evolution: use keyword arguments for new requirements
def log_events(*events, level: "INFO")
events.each { |event| puts "[#{level}] #{event}" }
end
Reference
Method Definition Syntax
Syntax | Description | Example |
---|---|---|
def method(*args) |
Captures all arguments | def process(*items) |
def method(req, *args) |
Required parameter first | def log(level, *messages) |
def method(*args, kw:) |
With required keyword | def build(*parts, format:) |
def method(opt = nil, *args) |
Optional parameter first | def render(template = nil, *data) |
def method(*args, **kwargs) |
With keyword arguments | def request(*path, **options) |
Method Call Syntax
Syntax | Description | Result |
---|---|---|
method(a, b, c) |
Direct arguments | args = [a, b, c] |
method(*array) |
Splat array | Array elements as separate args |
method(a, *array, b) |
Mixed splat | a first, array elements, then b |
method(*[], *array) |
Multiple splats | All arrays combined |
Argument Processing Order
- Required positional parameters - Filled first from left to right
- Optional parameters with defaults - Assigned if arguments available
- *Splat parameter (args) - Collects remaining positional arguments
- Required keyword arguments - Must be provided with explicit keys
- Optional keyword arguments - Use defaults if not provided
- **Double splat (kwargs) - Collects remaining keyword arguments
Common Patterns
Pattern | Use Case | Example |
---|---|---|
*args only |
Variable argument count | def sum(*numbers) |
Required + *args |
At least one argument | def join(separator, *parts) |
*args + keyword |
Optional config | def process(*items, format: :json) |
*args + **kwargs |
Full flexibility | def request(*path, **options) |
... forwarding |
Delegation (Ruby 2.7+) | def wrapper(...); target(...); end |
Array Methods on *args
Since *args
creates an Array, all array methods are available:
Method | Purpose | Example |
---|---|---|
args.empty? |
Check if no arguments | return if args.empty? |
args.length / args.size |
Argument count | raise "Too many" if args.size > 5 |
args.first / args.last |
Access endpoints | primary = args.first |
args[index] |
Access by position | second_arg = args[1] |
args.each |
Iterate arguments | args.each do |
args.map |
Transform arguments | results = args.map(&:upcase) |
args.select |
Filter arguments | valid = args.select(&:present?) |
Performance Characteristics
Operation | Complexity | Notes |
---|---|---|
Method call with *args |
O(n) | Creates new array each call |
Array access in *args |
O(1) | Standard array indexing |
Splat expansion | O(n) | Copies array elements |
Empty *args |
O(1) | Still creates empty array |
Error Types
Error | Cause | Example |
---|---|---|
ArgumentError |
Wrong argument count | Required params not satisfied |
NoMethodError |
Calling on wrong type | calculate_sum([1,2,3]) without splat |
TypeError |
Splat non-enumerable | method(*5) - can't splat integer |