CrackedRuby logo

CrackedRuby

Variable Arguments (*args)

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

  1. Required positional parameters - Filled first from left to right
  2. Optional parameters with defaults - Assigned if arguments available
  3. *Splat parameter (args) - Collects remaining positional arguments
  4. Required keyword arguments - Must be provided with explicit keys
  5. Optional keyword arguments - Use defaults if not provided
  6. **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