Overview
Ruby provides multiple mechanisms for defining method parameters, ranging from simple required positional arguments to complex keyword argument patterns. The language supports required parameters that must be provided when calling a method, optional parameters with default values, and variable-length parameter lists through splat operators.
Method parameters in Ruby fall into several categories: required positional parameters, optional positional parameters with defaults, required keyword arguments, optional keyword arguments, and catch-all parameters using splat operators. Ruby evaluates default parameter expressions at method call time, not definition time, which creates powerful dynamic behavior patterns.
def process_data(required_param, optional_param = "default", *rest_params,
required_keyword:, optional_keyword: nil, **keyword_rest)
puts "Required: #{required_param}"
puts "Optional: #{optional_param}"
puts "Rest: #{rest_params}"
puts "Required keyword: #{required_keyword}"
puts "Optional keyword: #{optional_keyword}"
puts "Keyword rest: #{keyword_rest}"
end
process_data("value", required_keyword: "must_provide")
# => Required: value
# => Optional: default
# => Rest: []
# => Required keyword: must_provide
# => Optional keyword:
# => Keyword rest: {}
The parameter system integrates closely with Ruby's method dispatch and argument binding mechanisms. When Ruby encounters a method call, it performs argument matching from left to right for positional parameters, then processes keyword arguments by name. This matching process follows strict rules that determine when arguments are valid or when Ruby raises ArgumentError
.
def flexible_method(a, b = 2, c = 3, *rest, x:, y: 10, **options)
[a, b, c, rest, x, y, options]
end
flexible_method(1, x: "required")
# => [1, 2, 3, [], "required", 10, {}]
flexible_method(1, 4, 5, 6, 7, x: "req", z: "extra")
# => [1, 4, 5, [6, 7], "req", 10, {:z=>"extra"}]
Ruby's parameter system becomes particularly powerful when combined with metaprogramming techniques. Methods can introspect their own parameter definitions using Method#parameters
, enabling dynamic argument processing and validation patterns.
Basic Usage
Required positional parameters form the foundation of Ruby method definitions. These parameters must be provided in the exact order specified, and Ruby raises ArgumentError
if any are missing. The parameter names become local variables within the method scope.
def calculate_interest(principal, rate, time)
principal * rate * time / 100
end
calculate_interest(1000, 5.5, 2)
# => 110.0
calculate_interest(1000, 5.5)
# => ArgumentError: wrong number of arguments (given 2, expected 3)
Optional positional parameters accept default values that Ruby evaluates when the corresponding argument is not provided. Default value expressions can reference previously defined parameters and call methods, creating dynamic defaults based on other arguments.
def create_user(name, email = nil, role = "user", created_at = Time.now)
{
name: name,
email: email || "#{name.downcase}@example.com",
role: role,
created_at: created_at
}
end
create_user("John")
# => {:name=>"John", :email=>"john@example.com", :role=>"user", :created_at=>2024-01-15 10:30:45 UTC}
create_user("Jane", "jane@custom.com", "admin")
# => {:name=>"Jane", :email=>"jane@custom.com", :role=>"admin", :created_at=>2024-01-15 10:30:45 UTC}
Keyword arguments provide explicit parameter naming at call sites, improving code readability and allowing arguments to be passed in any order. Required keyword arguments use a colon after the parameter name without a default value, while optional keyword arguments include default values.
def configure_database(host:, port: 5432, username:, password:,
ssl: true, timeout: 30)
connection_params = {
host: host,
port: port,
username: username,
password: password,
ssl: ssl,
timeout: timeout
}
puts "Connecting to #{host}:#{port} as #{username}"
puts "SSL: #{ssl ? 'enabled' : 'disabled'}"
connection_params
end
configure_database(host: "localhost", username: "admin", password: "secret")
# => Connecting to localhost:5432 as admin
# => SSL: enabled
configure_database(
password: "secret",
username: "admin",
host: "db.example.com",
port: 3306,
ssl: false
)
# => Connecting to db.example.com:3306 as admin
# => SSL: disabled
The splat operator (*
) captures variable numbers of positional arguments into an array, while the double splat operator (**
) captures additional keyword arguments into a hash. These operators enable methods to accept flexible argument lists while maintaining type safety for known parameters.
def log_event(level, message, *details, timestamp: Time.now, **metadata)
event = {
level: level,
message: message,
details: details,
timestamp: timestamp,
metadata: metadata
}
puts "[#{timestamp}] #{level.upcase}: #{message}"
details.each_with_index do |detail, index|
puts " Detail #{index + 1}: #{detail}"
end
metadata.each do |key, value|
puts " #{key}: #{value}"
end
event
end
log_event("info", "User login", "192.168.1.1", "Chrome/96.0",
user_id: 123, session_id: "abc456", source: "web")
# => [2024-01-15 10:30:45 UTC] INFO: User login
# => Detail 1: 192.168.1.1
# => Detail 2: Chrome/96.0
# => user_id: 123
# => session_id: abc456
# => source: web
Advanced Usage
Ruby's parameter system supports complex patterns that combine multiple parameter types in sophisticated ways. The order of parameters must follow Ruby's strict sequence: required positional, optional positional, splat, required keyword, optional keyword, and double splat parameters.
class DataProcessor
def process(source, format = :json, *transformers,
output_dir:, compression: :gzip,
validate: true, **processing_options)
pipeline = ProcessingPipeline.new(
source: source,
format: format,
output_dir: output_dir,
compression: compression,
validate: validate
)
transformers.each do |transformer|
case transformer
when Symbol
pipeline.add_transformer(transformer)
when Hash
transformer.each { |name, config| pipeline.add_transformer(name, config) }
when Proc
pipeline.add_custom_transformer(&transformer)
end
end
processing_options.each do |option, value|
pipeline.set_option(option, value)
end
pipeline.execute
end
end
processor = DataProcessor.new
result = processor.process(
"data.csv",
:csv,
:normalize_headers,
{ clean_data: { remove_nulls: true, trim_strings: true } },
->(row) { row.transform_values(&:strip) },
output_dir: "/tmp/processed",
compression: :bzip2,
batch_size: 1000,
parallel: true,
memory_limit: "2GB"
)
Parameter destructuring allows methods to extract specific values from array and hash arguments directly in the parameter list. This pattern works particularly well with data processing and API wrapper methods.
def analyze_coordinates((x, y, z = 0), scale: 1.0, origin: [0, 0, 0])
scaled_x = (x - origin[0]) * scale
scaled_y = (y - origin[1]) * scale
scaled_z = (z - origin[2]) * scale
distance = Math.sqrt(scaled_x**2 + scaled_y**2 + scaled_z**2)
{
original: [x, y, z],
scaled: [scaled_x, scaled_y, scaled_z],
distance_from_origin: distance
}
end
analyze_coordinates([10, 20], scale: 2.0, origin: [5, 5, 0])
# => {:original=>[10, 20, 0], :scaled=>[10.0, 30.0, 0.0], :distance_from_origin=>31.622776601683793}
# Works with method calls that return arrays
def get_position
[15, 25, 30]
end
analyze_coordinates(get_position, scale: 0.5)
# => {:original=>[15, 25, 30], :scaled=>[7.5, 12.5, 15.0], :distance_from_origin=>20.616601717798213}
Block parameters represent a special category of optional parameters in Ruby. Methods can accept blocks using yield
or by declaring an explicit block parameter with the &
operator. Block parameters enable powerful callback and iteration patterns.
def batch_process(items, batch_size: 100, &processor)
raise ArgumentError, "Block required for processing" unless block_given?
results = []
errors = []
items.each_slice(batch_size).with_index do |batch, batch_index|
puts "Processing batch #{batch_index + 1} (#{batch.size} items)"
begin
batch_results = batch.map.with_index do |item, item_index|
global_index = batch_index * batch_size + item_index
processor.call(item, global_index, batch_index)
end
results.concat(batch_results)
rescue => error
errors << { batch: batch_index, error: error, items: batch }
puts "Error in batch #{batch_index + 1}: #{error.message}"
end
end
{ results: results, errors: errors, processed: results.size }
end
data = (1..250).to_a
result = batch_process(data, batch_size: 50) do |number, global_idx, batch_idx|
processed_value = number * 2 + rand(10)
{
original: number,
processed: processed_value,
indices: { global: global_idx, batch: batch_idx }
}
end
puts "Processed #{result[:processed]} items with #{result[:errors].size} batch errors"
Metaprogramming with parameters enables dynamic method generation based on parameter patterns. Ruby's Method#parameters
method returns detailed information about a method's parameter structure, enabling parameter validation and dynamic dispatch patterns.
module ParameterIntrospection
def self.analyze_method(object, method_name)
method = object.method(method_name)
params = method.parameters
analysis = {
required_positional: [],
optional_positional: [],
rest_positional: nil,
required_keyword: [],
optional_keyword: [],
rest_keyword: nil,
block: false
}
params.each do |type, name|
case type
when :req
analysis[:required_positional] << name
when :opt
analysis[:optional_positional] << name
when :rest
analysis[:rest_positional] = name
when :keyreq
analysis[:required_keyword] << name
when :key
analysis[:optional_keyword] << name
when :keyrest
analysis[:rest_keyword] = name
when :block
analysis[:block] = true
end
end
analysis
end
def self.validate_call(object, method_name, *args, **kwargs, &block)
analysis = analyze_method(object, method_name)
errors = []
# Check required positional arguments
if args.size < analysis[:required_positional].size
missing = analysis[:required_positional][args.size..-1]
errors << "Missing required arguments: #{missing.join(', ')}"
end
# Check maximum positional arguments (if no rest parameter)
max_positional = analysis[:required_positional].size + analysis[:optional_positional].size
if !analysis[:rest_positional] && args.size > max_positional
errors << "Too many positional arguments (given #{args.size}, expected #{max_positional})"
end
# Check required keyword arguments
missing_keywords = analysis[:required_keyword] - kwargs.keys
unless missing_keywords.empty?
errors << "Missing required keyword arguments: #{missing_keywords.join(', ')}"
end
# Check unknown keyword arguments (if no rest keyword parameter)
if !analysis[:rest_keyword]
known_keywords = analysis[:required_keyword] + analysis[:optional_keyword]
unknown_keywords = kwargs.keys - known_keywords
unless unknown_keywords.empty?
errors << "Unknown keyword arguments: #{unknown_keywords.join(', ')}"
end
end
if errors.empty?
object.send(method_name, *args, **kwargs, &block)
else
raise ArgumentError, errors.join('; ')
end
end
end
# Example usage with validation
class Calculator
def compute(operation, a, b = 1, precision: 2, debug: false)
result = case operation
when :add then a + b
when :multiply then a * b
when :divide then a.to_f / b
else raise ArgumentError, "Unknown operation: #{operation}"
end
puts "Computing #{operation}(#{a}, #{b}) = #{result}" if debug
result.round(precision)
end
end
calc = Calculator.new
analysis = ParameterIntrospection.analyze_method(calc, :compute)
puts analysis
# => {:required_positional=>[:operation, :a], :optional_positional=>[:b], :rest_positional=>nil, :required_keyword=>[], :optional_keyword=>[:precision, :debug], :rest_keyword=>nil, :block=>false}
ParameterIntrospection.validate_call(calc, :compute, :multiply, 5, 3, precision: 4, debug: true)
# => Computing multiply(5, 3) = 15
# => 15.0
Common Pitfalls
Default parameter evaluation occurs at method call time, not method definition time, which can create unexpected behavior when using mutable objects or time-dependent values as defaults. This timing difference becomes particularly problematic with shared mutable defaults.
# Problematic: shared mutable default
def add_item(item, collection = [])
collection << item
collection
end
list1 = add_item("first") # => ["first"]
list2 = add_item("second") # => ["first", "second"] - unexpected!
list3 = add_item("third") # => ["first", "second", "third"] - same array!
# Correct approach: use nil and initialize inside method
def add_item_safe(item, collection = nil)
collection ||= []
collection << item
collection
end
safe1 = add_item_safe("first") # => ["first"]
safe2 = add_item_safe("second") # => ["second"] - separate array
safe3 = add_item_safe("third") # => ["third"] - separate array
The same timing issue affects time-based defaults and other dynamic values. Default expressions are evaluated fresh for each method call, but object references can be shared in unexpected ways.
# Time evaluation happens at call time, not definition time
def log_message(text, timestamp = Time.now)
puts "[#{timestamp}] #{text}"
end
# Define method at 10:00
sleep 1
log_message("First") # Prints current time, not 10:00
sleep 1
log_message("Second") # Prints different current time
# Hash defaults can be shared unexpectedly
def process_options(data, options = {})
options[:processed_at] = Time.now
options[:data_size] = data.size
puts options
options
end
result1 = process_options("hello")
# => {:processed_at=>2024-01-15 10:30:45 UTC, :data_size=>5}
result2 = process_options("world")
# => {:processed_at=>2024-01-15 10:30:46 UTC, :data_size=>5, :processed_at=>2024-01-15 10:30:45 UTC, :data_size=>5}
# Hash contains data from previous call!
Parameter order restrictions in Ruby require specific sequencing that can create confusion when refactoring methods. Adding new parameter types to existing methods often requires reordering all parameters to maintain Ruby's syntax requirements.
# Original method
def original_method(a, b = 2)
[a, b]
end
# Adding required keyword - this works
def updated_method(a, b = 2, required_keyword:)
[a, b, required_keyword]
end
# But adding splat parameters requires reordering
# This won't work - syntax error
# def broken_method(a, b = 2, required_keyword:, *rest)
# Correct order: positional, optional, splat, keyword required, keyword optional, keyrest
def correct_method(a, *rest, b: 2, required_keyword:, **options)
{
required_pos: a,
rest_pos: rest,
optional_kw: b,
required_kw: required_keyword,
rest_kw: options
}
end
correct_method(1, 2, 3, required_keyword: "needed", extra: "value")
# => {:required_pos=>1, :rest_pos=>[2, 3], :optional_kw=>2, :required_kw=>"needed", :rest_kw=>{:extra=>"value"}}
Keyword argument compatibility changes between Ruby versions create migration challenges. Ruby 3.0 introduced automatic separation of positional and keyword arguments, which can break code that relied on hash arguments being treated as keyword arguments.
def process_user_data(name, age, options = {})
puts "Name: #{name}, Age: #{age}"
puts "Options: #{options}"
end
# This works in all Ruby versions
process_user_data("John", 25, { role: "admin", active: true })
# This worked in Ruby 2.x but may cause issues in Ruby 3+
def modern_process_user(name, age:, **options)
puts "Name: #{name}, Age: #{age}"
puts "Options: #{options}"
end
# Ruby 3+ requires explicit keyword syntax
modern_process_user("John", age: 25, role: "admin", active: true)
# Hash argument no longer automatically converts to keywords in Ruby 3
user_data = { age: 25, role: "admin", active: true }
# This may not work as expected:
# modern_process_user("John", user_data)
# Explicit conversion required:
modern_process_user("John", **user_data)
Method parameter introspection can return misleading information when dealing with methods defined through metaprogramming or delegation. The parameters
method reflects the actual method definition, which may not match the expected calling interface.
class DynamicMethods
[:get, :post, :put, :delete].each do |verb|
define_method("#{verb}_request") do |url, data = nil, **headers|
puts "#{verb.upcase} #{url}"
puts "Data: #{data}" if data
puts "Headers: #{headers}" unless headers.empty?
end
end
def method_missing(method_name, *args, **kwargs, &block)
if method_name.to_s.end_with?('_api_call')
api_method = method_name.to_s.sub('_api_call', '')
puts "Dynamic API call: #{api_method}"
puts "Args: #{args}, Kwargs: #{kwargs}"
else
super
end
end
end
dynamic = DynamicMethods.new
# Introspection works for defined methods
puts dynamic.method(:get_request).parameters
# => [[:req, :url], [:opt, :data], [:keyrest, :headers]]
# But method_missing methods don't show up in introspection
begin
puts dynamic.method(:custom_api_call).parameters
rescue NameError => e
puts "Method not found for introspection: #{e.message}"
end
# The method still works via method_missing
dynamic.custom_api_call("endpoint", token: "abc123")
# => Dynamic API call: custom
# => Args: ["endpoint"], Kwargs: {:token=>"abc123"}
Argument forwarding in Ruby 2.7+ introduces the ...
syntax for passing arguments through wrapper methods, but this can create debugging challenges when argument errors occur deep in call stacks.
class RequestWrapper
def self.make_request(...)
log_request(...)
send_http_request(...)
end
def self.log_request(method, url, **options)
puts "[LOG] #{method.upcase} #{url}"
puts "[LOG] Options: #{options}" unless options.empty?
end
def self.send_http_request(method, url, body: nil, headers: {}, timeout: 30)
puts "[HTTP] Sending #{method.upcase} to #{url}"
puts "[HTTP] Body: #{body}" if body
puts "[HTTP] Headers: #{headers}" unless headers.empty?
puts "[HTTP] Timeout: #{timeout}s"
# Simulate response
{ status: 200, body: "Success" }
end
end
# This works correctly
RequestWrapper.make_request(:get, "https://api.example.com/users",
headers: { "Authorization" => "Bearer token" },
timeout: 60)
# But argument errors can be confusing with forwarding
begin
RequestWrapper.make_request(:get) # Missing required URL
rescue ArgumentError => e
puts "Error with argument forwarding: #{e.message}"
puts "Difficult to trace which method in chain failed"
end
# More explicit approach for better error handling
class ExplicitRequestWrapper
def self.make_request(method, url, **options)
validate_arguments(method, url, **options)
log_request(method, url, **options)
send_http_request(method, url, **options)
rescue ArgumentError => e
raise ArgumentError, "RequestWrapper.make_request: #{e.message}"
end
def self.validate_arguments(method, url, **options)
raise ArgumentError, "method is required" if method.nil?
raise ArgumentError, "url is required" if url.nil? || url.empty?
valid_methods = [:get, :post, :put, :delete, :patch]
unless valid_methods.include?(method.to_sym)
raise ArgumentError, "method must be one of #{valid_methods}"
end
end
def self.log_request(method, url, **options)
puts "[LOG] #{method.upcase} #{url}"
puts "[LOG] Options: #{options}" unless options.empty?
end
def self.send_http_request(method, url, body: nil, headers: {}, timeout: 30, **extra_options)
unless extra_options.empty?
raise ArgumentError, "Unknown options: #{extra_options.keys.join(', ')}"
end
puts "[HTTP] Sending #{method.upcase} to #{url}"
puts "[HTTP] Body: #{body}" if body
puts "[HTTP] Headers: #{headers}" unless headers.empty?
puts "[HTTP] Timeout: #{timeout}s"
{ status: 200, body: "Success" }
end
end
Reference
Parameter Types
Parameter Type | Syntax | Required | Description |
---|---|---|---|
Required Positional | name |
Yes | Must be provided in order, no default value |
Optional Positional | name = default |
No | Uses default if not provided |
Rest Positional | *name |
No | Captures extra positional args as Array |
Required Keyword | name: |
Yes | Must be provided with keyword syntax |
Optional Keyword | name: default |
No | Uses default if not provided |
Rest Keyword | **name |
No | Captures extra keyword args as Hash |
Block Parameter | &name |
No | Captures block as Proc object |
Parameter Order Requirements
Parameters must be defined in this exact order:
- Required positional parameters
- Optional positional parameters
- Rest positional parameter (
*args
) - Required keyword parameters
- Optional keyword parameters
- Rest keyword parameter (
**kwargs
) - Block parameter (
&block
)
Method#parameters Return Values
Symbol | Description | Example Parameter |
---|---|---|
:req |
Required positional | name |
:opt |
Optional positional | name = "default" |
:rest |
Rest positional | *args |
:keyreq |
Required keyword | name: |
:key |
Optional keyword | name: "default" |
:keyrest |
Rest keyword | **kwargs |
:block |
Block parameter | &block |
Default Value Evaluation
Context | Evaluation Time | Shared State |
---|---|---|
Literal values | Call time | No sharing |
Method calls | Each call | Fresh evaluation |
Mutable objects | Definition time | Shared across calls |
Time expressions | Each call | Fresh timestamp |
Common ArgumentError Messages
Error Condition | Message Pattern |
---|---|
Missing required positional | wrong number of arguments (given X, expected Y) |
Missing required keyword | missing keyword: name |
Unknown keyword argument | unknown keyword: name |
Too many positional args | wrong number of arguments (given X, expected Y) |
Argument Forwarding Syntax
Ruby Version | Syntax | Forwards |
---|---|---|
2.7+ | def wrapper(...); target(...); end |
All arguments and block |
2.6- | def wrapper(*args, **kwargs, &block); target(*args, **kwargs, &block); end |
Manual forwarding |
Block Parameter Patterns
Pattern | Syntax | Usage |
---|---|---|
Implicit block | yield |
Most common, uses block_given? |
Explicit block | &block |
When block needs to be stored/passed |
Proc parameter | block = nil |
When block is optional argument |
Destructuring Assignment
Parameter | Matches | Example |
---|---|---|
(a, b) |
Array with 2+ elements | [1, 2, 3] → a=1, b=2 |
(a, b, c) |
Array with 3+ elements | [1, 2] → a=1, b=2, c=nil |
(a, *rest) |
Array with 1+ elements | [1, 2, 3] → a=1, rest=[2, 3] |
((x, y)) |
Nested array | [[1, 2], 3] → x=1, y=2 |
Performance Characteristics
Parameter Type | Call Overhead | Memory Impact |
---|---|---|
Required positional | Minimal | Stack allocated |
Optional positional | Low | Stack allocated |
Rest positional | Moderate | Array allocation |
Required keyword | Low | Hash lookup |
Optional keyword | Moderate | Hash allocation |
Rest keyword | High | Hash allocation + merge |