Overview
Ruby method parameters provide flexible mechanisms for passing data to methods through multiple parameter types. The language supports positional parameters, keyword parameters, splat parameters for variable arguments, double splat parameters for keyword argument capture, and block parameters for code execution.
The parameter system operates through method signatures that define parameter names, types, default values, and constraints. Ruby evaluates parameters left-to-right during method calls, with specific precedence rules governing parameter matching and argument assignment.
def process_data(required, optional = "default", *args, keyword:, optional_kw: nil, **kwargs, &block)
puts "Required: #{required}"
puts "Optional: #{optional}"
puts "Splat args: #{args}"
puts "Required keyword: #{keyword}"
puts "Optional keyword: #{optional_kw}"
puts "Keyword hash: #{kwargs}"
block.call if block_given?
end
process_data("value", "custom", "extra1", "extra2", keyword: "req", other: "data") { puts "Block executed" }
# Required: value
# Optional: custom
# Splat args: ["extra1", "extra2"]
# Required keyword: req
# Optional keyword:
# Keyword hash: {:other=>"data"}
# Block executed
Method parameters integrate with Ruby's dynamic nature through argument forwarding, parameter introspection via Method
objects, and runtime parameter validation. The system handles type coercion, parameter binding, and scope management during method invocation.
class DataProcessor
def initialize(strategy: :basic, **options)
@strategy = strategy
@options = options
end
def process(*items, transform: nil, &block)
items.map do |item|
result = apply_strategy(item)
result = transform.call(result) if transform
result = block.call(result) if block_given?
result
end
end
private
def apply_strategy(item)
case @strategy
when :basic then item.to_s.upcase
when :reverse then item.to_s.reverse
else item
end
end
end
processor = DataProcessor.new(strategy: :reverse, debug: true)
results = processor.process("hello", "world", transform: ->(x) { x + "!" }) { |x| "[#{x}]" }
# => ["[!olleh]", "[!dlrow]"]
Basic Usage
Ruby methods accept parameters through several distinct mechanisms, each serving different use cases and providing varying degrees of flexibility.
Positional Parameters
Positional parameters receive arguments based on their position in the method signature. Required positional parameters must receive arguments, while optional positional parameters use default values when arguments are omitted.
def create_user(name, email, role = "user", active = true)
{
name: name,
email: email,
role: role,
active: active,
created_at: Time.now
}
end
user1 = create_user("Alice", "alice@example.com")
# => {:name=>"Alice", :email=>"alice@example.com", :role=>"user", :active=>true, :created_at=>...}
user2 = create_user("Bob", "bob@example.com", "admin", false)
# => {:name=>"Bob", :email=>"bob@example.com", :role=>"admin", :active=>false, :created_at=>...}
Default parameter values are evaluated at method call time, not method definition time. This enables dynamic default values but requires caution with mutable objects.
def log_message(text, timestamp = Time.now, tags = [])
tags << "logged" # Modifies the default array
puts "#{timestamp}: #{text} [#{tags.join(', ')}]"
tags
end
log_message("First call") # => ["logged"]
log_message("Second call") # => ["logged", "logged"] - Same array object
log_message("Third call", Time.now, ["custom"]) # => ["custom", "logged"]
Keyword Parameters
Keyword parameters provide named argument passing, improving method call clarity and enabling flexible parameter ordering. Ruby distinguishes between required and optional keyword parameters.
def configure_database(host:, port: 5432, username:, password:, ssl: false, **options)
config = {
host: host,
port: port,
username: username,
password: password,
ssl: ssl
}.merge(options)
puts "Connecting to #{config[:host]}:#{config[:port]}"
puts "SSL: #{config[:ssl] ? 'enabled' : 'disabled'}"
puts "Additional options: #{options}" unless options.empty?
config
end
# Arguments can be provided in any order
db_config = configure_database(
username: "admin",
host: "localhost",
password: "secret",
timeout: 30,
pool_size: 10
)
Keyword parameters support complex validation patterns and conditional defaults through method implementation.
def process_payment(amount:, currency: "USD", method: :credit_card, **details)
raise ArgumentError, "Amount must be positive" if amount <= 0
case method
when :credit_card
required_fields = [:card_number, :expiry, :cvv]
missing = required_fields - details.keys
raise ArgumentError, "Missing credit card fields: #{missing}" unless missing.empty?
when :bank_transfer
required_fields = [:account_number, :routing_number]
missing = required_fields - details.keys
raise ArgumentError, "Missing bank transfer fields: #{missing}" unless missing.empty?
end
{
amount: amount,
currency: currency,
method: method,
details: details,
processed_at: Time.now
}
end
# Valid credit card payment
payment1 = process_payment(
amount: 100.00,
method: :credit_card,
card_number: "1234-5678-9012-3456",
expiry: "12/25",
cvv: "123"
)
Variable Arguments
Splat parameters capture variable numbers of positional arguments into arrays, while double splat parameters capture variable keyword arguments into hashes.
def analyze_data(*datasets, format: :json, **options)
results = {}
datasets.each_with_index do |data, index|
key = "dataset_#{index + 1}"
results[key] = {
size: data.size,
type: data.class.name,
format: format
}.merge(options)
end
results
end
analysis = analyze_data(
[1, 2, 3, 4, 5],
{"a" => 1, "b" => 2},
"sample string",
format: :xml,
validator: "strict",
encoding: "utf-8"
)
# => {
# :dataset_1 => {:size=>5, :type=>"Array", :format=>:xml, :validator=>"strict", :encoding=>"utf-8"},
# :dataset_2 => {:size=>2, :type=>"Hash", :format=>:xml, :validator=>"strict", :encoding=>"utf-8"},
# :dataset_3 => {:size=>13, :type=>"String", :format=>:xml, :validator=>"strict", :encoding=>"utf-8"}
# }
Advanced Usage
Ruby method parameters support sophisticated patterns through parameter forwarding, metaprogramming integration, and dynamic parameter manipulation.
Parameter Forwarding
Ruby provides argument forwarding operators that delegate parameters to other methods while preserving parameter types and structure. The ...
operator forwards all arguments, while individual forwarding preserves specific parameter categories.
class APIWrapper
def initialize(base_url, **default_options)
@base_url = base_url
@default_options = default_options
end
def get(path, **options, &block)
request(:get, path, **@default_options.merge(options), &block)
end
def post(path, body = nil, **options, &block)
request(:post, path, body: body, **@default_options.merge(options), &block)
end
# Forward all arguments to underlying HTTP method
def request(method, path, ...)
http_request(method, "#{@base_url}#{path}", ...)
end
private
def http_request(method, url, body: nil, headers: {}, timeout: 30, &block)
puts "#{method.upcase} #{url}"
puts "Headers: #{headers}" unless headers.empty?
puts "Body: #{body}" if body
puts "Timeout: #{timeout}"
result = { status: 200, data: "response" }
block.call(result) if block_given?
result
end
end
api = APIWrapper.new("https://api.example.com", headers: { "Authorization" => "Bearer token" })
response = api.get("/users", timeout: 60) do |result|
puts "Processing response: #{result[:status]}"
end
Metaprogramming with Parameters
Method parameters integrate with Ruby's metaprogramming capabilities through Method
objects, parameter introspection, and dynamic method definition.
class DynamicProcessor
def self.create_processor(name, *required_params, **default_options, &validator)
define_method("process_#{name}") do |*args, **options, &block|
# Validate required parameters
if args.length < required_params.length
missing = required_params[args.length..-1]
raise ArgumentError, "Missing required parameters: #{missing}"
end
# Build parameter hash
params = {}
required_params.each_with_index { |param, i| params[param] = args[i] }
# Merge options with defaults
final_options = default_options.merge(options)
# Apply validation if provided
if validator
validation_result = validator.call(params, final_options)
raise ArgumentError, "Validation failed: #{validation_result}" if validation_result.is_a?(String)
end
# Execute processing logic
result = {
processor: name,
parameters: params,
options: final_options,
processed_at: Time.now
}
block.call(result) if block_given?
result
end
end
# Create processors with different parameter configurations
create_processor(:user_data, :name, :email, format: :json, validate: true) do |params, options|
return "Invalid email format" unless params[:email].include?("@")
return "Name too short" if params[:name].length < 2
true
end
create_processor(:file_data, :path, :content, encoding: "utf-8", compress: false) do |params, options|
return "File path cannot be empty" if params[:path].empty?
return "Content must be string" unless params[:content].is_a?(String)
true
end
end
processor = DynamicProcessor.new
user_result = processor.process_user_data("Alice Smith", "alice@example.com", validate: false) do |result|
puts "User processed: #{result[:parameters][:name]}"
end
Complex Parameter Patterns
Advanced parameter patterns combine multiple parameter types with validation, transformation, and conditional logic.
class ConfigurationBuilder
def initialize
@configurations = {}
end
def add_config(name, *layers, required: [], optional: {}, transforms: {}, &validator)
# Merge configuration layers
config = layers.reduce({}) do |merged, layer|
case layer
when Hash then merged.merge(layer)
when String then merged.merge(load_config_file(layer))
else raise ArgumentError, "Invalid configuration layer: #{layer.class}"
end
end
# Apply optional defaults
config = optional.merge(config)
# Validate required keys
missing_required = required - config.keys
raise ArgumentError, "Missing required configuration: #{missing_required}" unless missing_required.empty?
# Apply transformations
transforms.each do |key, transform|
config[key] = transform.call(config[key]) if config.key?(key)
end
# Custom validation
if validator
validation_errors = validator.call(config)
raise ArgumentError, "Configuration validation failed: #{validation_errors}" if validation_errors
end
@configurations[name] = config
end
def get_config(name, **overrides)
base_config = @configurations[name] or raise ArgumentError, "Unknown configuration: #{name}"
base_config.merge(overrides)
end
private
def load_config_file(path)
# Simulate file loading
case File.extname(path)
when '.json' then { loaded_from: path, format: 'json' }
when '.yaml' then { loaded_from: path, format: 'yaml' }
else {}
end
end
end
builder = ConfigurationBuilder.new
builder.add_config(
:database,
{ host: "localhost", port: 5432 },
"/etc/app/database.json",
{ username: "admin", password: "secret" },
required: [:host, :username, :password],
optional: { timeout: 30, pool_size: 5, ssl: false },
transforms: {
host: ->(h) { h.downcase.strip },
port: ->(p) { p.to_i },
ssl: ->(s) { !!s }
}
) do |config|
errors = []
errors << "Port must be positive integer" unless config[:port].positive?
errors << "Username cannot be empty" if config[:username].empty?
errors.empty? ? nil : errors
end
db_config = builder.get_config(:database, pool_size: 10, ssl: true)
Parameter Object Patterns
Complex parameter handling benefits from parameter object patterns that encapsulate parameter validation, transformation, and access.
class SearchParameters
VALID_ORDERS = %w[asc desc].freeze
VALID_FIELDS = %w[name email created_at updated_at].freeze
attr_reader :query, :page, :per_page, :order, :order_by, :filters
def initialize(query: nil, page: 1, per_page: 10, order: "asc", order_by: "created_at", **filters)
@query = normalize_query(query)
@page = validate_page(page)
@per_page = validate_per_page(per_page)
@order = validate_order(order)
@order_by = validate_order_by(order_by)
@filters = normalize_filters(filters)
end
def offset
(page - 1) * per_page
end
def to_sql_conditions
conditions = []
parameters = []
if query
conditions << "name ILIKE ? OR email ILIKE ?"
parameters.concat(["%#{query}%", "%#{query}%"])
end
filters.each do |field, value|
next unless VALID_FIELDS.include?(field.to_s)
conditions << "#{field} = ?"
parameters << value
end
[conditions.join(" AND "), parameters]
end
def to_h
{
query: query,
page: page,
per_page: per_page,
order: order,
order_by: order_by,
offset: offset,
filters: filters
}
end
private
def normalize_query(query)
return nil if query.nil? || query.to_s.strip.empty?
query.to_s.strip
end
def validate_page(page)
page_int = Integer(page)
raise ArgumentError, "Page must be positive" unless page_int.positive?
page_int
rescue TypeError, ArgumentError => e
raise ArgumentError, "Invalid page parameter: #{e.message}"
end
def validate_per_page(per_page)
per_page_int = Integer(per_page)
raise ArgumentError, "Per page must be between 1 and 100" unless (1..100).cover?(per_page_int)
per_page_int
rescue TypeError, ArgumentError => e
raise ArgumentError, "Invalid per_page parameter: #{e.message}"
end
def validate_order(order)
order_str = order.to_s.downcase
raise ArgumentError, "Order must be 'asc' or 'desc'" unless VALID_ORDERS.include?(order_str)
order_str
end
def validate_order_by(order_by)
order_by_str = order_by.to_s.downcase
raise ArgumentError, "Invalid order_by field" unless VALID_FIELDS.include?(order_by_str)
order_by_str
end
def normalize_filters(filters)
filters.transform_keys(&:to_s).select { |key, _| VALID_FIELDS.include?(key) }
end
end
def search_users(**params)
search_params = SearchParameters.new(**params)
puts "Search parameters: #{search_params.to_h}"
conditions, parameters = search_params.to_sql_conditions
puts "SQL: SELECT * FROM users WHERE #{conditions} ORDER BY #{search_params.order_by} #{search_params.order.upcase} LIMIT #{search_params.per_page} OFFSET #{search_params.offset}"
puts "Parameters: #{parameters}"
# Simulate database results
{ results: [], total: 0, page: search_params.page, per_page: search_params.per_page }
end
results = search_users(query: "alice", page: 2, per_page: 20, order: "desc", order_by: "created_at", role: "admin")
Common Pitfalls
Ruby method parameters present several common pitfalls that can lead to unexpected behavior, parameter conflicts, and runtime errors.
Mutable Default Parameters
Default parameter values are shared across method calls when they reference the same object. This creates unexpected state persistence between method invocations.
# Problematic implementation
def add_item(item, collection = [])
collection << item
puts "Collection now contains: #{collection}"
collection
end
list1 = add_item("first") # => ["first"]
list2 = add_item("second") # => ["first", "second"] - Unexpected!
list3 = add_item("third") # => ["first", "second", "third"] - All calls share same array
# Correct implementation
def add_item_safely(item, collection = nil)
collection ||= []
collection << item
puts "Collection now contains: #{collection}"
collection
end
safe_list1 = add_item_safely("first") # => ["first"]
safe_list2 = add_item_safely("second") # => ["second"] - Each call gets fresh array
safe_list3 = add_item_safely("third") # => ["third"]
The same issue occurs with other mutable objects including hashes, custom objects, and even time-based defaults that should be evaluated per call.
# Time evaluation pitfall
class Logger
# Wrong: timestamp evaluated once at class definition
def log_wrong(message, timestamp = Time.now)
puts "#{timestamp}: #{message}"
end
# Correct: timestamp evaluated per method call
def log_correct(message, timestamp = nil)
timestamp ||= Time.now
puts "#{timestamp}: #{message}"
end
end
logger = Logger.new
logger.log_wrong("First message")
sleep(2)
logger.log_wrong("Second message") # Same timestamp as first!
logger.log_correct("First message")
sleep(2)
logger.log_correct("Second message") # Different timestamps
Parameter Order Dependencies
Mixing positional and keyword parameters creates order dependencies that can cause confusing errors and unexpected argument assignment.
# Problematic parameter mixing
def process_data(name, *items, required_option:, default_option: "default", **other_options)
puts "Name: #{name}"
puts "Items: #{items}"
puts "Required: #{required_option}"
puts "Default: #{default_option}"
puts "Other: #{other_options}"
end
# This works as expected
process_data("test", "item1", "item2", required_option: "value")
# This causes confusion - where does "item3" go?
process_data("test", "item1", "item2", required_option: "value", extra: "data")
# Even more confusing with mixed argument types
def configure_service(name, port = 3000, *middleware, ssl: false, **options)
# Parameter assignment becomes unclear
end
# Better design - clear separation
def configure_service_clear(name:, port: 3000, middleware: [], ssl: false, **options)
puts "Service: #{name} on port #{port}"
puts "Middleware: #{middleware}"
puts "SSL: #{ssl}"
puts "Options: #{options}"
end
configure_service_clear(
name: "api",
port: 8080,
middleware: ["auth", "cors"],
ssl: true,
timeout: 30
)
Splat Parameter Gotchas
Splat parameters capture arguments in ways that can mask method signature errors and create unexpected parameter consumption.
def problematic_splat(first, *middle, last, **options)
puts "First: #{first}"
puts "Middle: #{middle}"
puts "Last: #{last}"
puts "Options: #{options}"
end
# Ruby requires at least 2 positional arguments for this to work
problematic_splat("only_one") # ArgumentError: wrong number of arguments
# Splat consumes more than expected
problematic_splat("a", "b", "c", "d", "e")
# First: a
# Middle: ["b", "c", "d"]
# Last: e
# Options: {}
# Keyword arguments don't go where expected
problematic_splat("a", "b", key: "value")
# ArgumentError - Ruby can't distinguish positional from keyword args
# Better approach with clearer intent
def clear_parameters(first:, last:, middle: [], **options)
puts "First: #{first}"
puts "Middle: #{middle}"
puts "Last: #{last}"
puts "Options: #{options}"
end
clear_parameters(first: "a", last: "e", middle: ["b", "c", "d"], extra: "option")
Block Parameter Conflicts
Block parameters can conflict with local variables and create scoping issues that mask bugs or cause unexpected behavior.
def process_items(items, &processor)
results = []
items.each do |item|
# Block parameter shadowing
result = processor.call(item) if processor
results << result
end
results
end
# Variable shadowing in block
items = ["a", "b", "c"]
results = [] # This variable gets shadowed
processed = process_items(items) do |item|
results = item.upcase # Creates new local variable, doesn't modify outer results
results + "!"
end
puts processed # => ["A!", "B!", "C!"]
puts results # => [] - Original results unchanged due to shadowing
# Correct approach - avoid shadowing
def process_items_safe(items)
results = []
items.each do |item|
result = yield(item) if block_given?
results << result if result
end
results
end
outer_results = []
processed_safe = process_items_safe(items) do |item|
transformed = item.upcase + "!"
outer_results << "processed: #{transformed}" # Explicit outer scope access
transformed
end
Parameter Validation Edge Cases
Parameter validation often misses edge cases related to type coercion, nil handling, and boundary conditions.
class UserService
def create_user(name:, email:, age: nil, tags: [], **metadata)
# Insufficient validation
raise ArgumentError, "Name required" if name.empty?
raise ArgumentError, "Invalid email" unless email.include?("@")
raise ArgumentError, "Age must be positive" if age && age <= 0
# Edge cases not handled:
# - name could be nil (nil.empty? raises NoMethodError)
# - email could be just "@"
# - age could be a string that converts to number
# - tags could be nil instead of array
# - metadata could contain invalid keys/values
end
# Robust validation handles edge cases
def create_user_robust(name:, email:, age: nil, tags: [], **metadata)
# Handle nil and type issues
name = name.to_s.strip
raise ArgumentError, "Name cannot be blank" if name.empty?
email = email.to_s.strip.downcase
raise ArgumentError, "Invalid email format" unless valid_email?(email)
if age
age = Integer(age) rescue (raise ArgumentError, "Age must be a number")
raise ArgumentError, "Age must be between 0 and 150" unless (0..150).cover?(age)
end
tags = Array(tags).map(&:to_s).uniq
# Validate metadata keys
invalid_keys = metadata.keys.reject { |k| k.to_s.match?(/\A[a-z_][a-z0-9_]*\z/) }
raise ArgumentError, "Invalid metadata keys: #{invalid_keys}" unless invalid_keys.empty?
{ name: name, email: email, age: age, tags: tags, metadata: metadata }
end
private
def valid_email?(email)
email.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
end
end
service = UserService.new
# These should all be handled gracefully
begin
service.create_user_robust(name: nil, email: "invalid", age: "not_a_number")
rescue ArgumentError => e
puts "Validation error: #{e.message}"
end
# Valid creation
user = service.create_user_robust(
name: " Alice Smith ",
email: "ALICE@EXAMPLE.COM",
age: "25",
tags: ["admin", "admin", "user"], # Duplicates removed
department: "engineering",
hire_date: "2024-01-15"
)
puts user
# => {:name=>"Alice Smith", :email=>"alice@example.com", :age=>25, :tags=>["admin", "user"], :metadata=>{:department=>"engineering", :hire_date=>"2024-01-15"}}
Reference
Parameter Types
Parameter Type | Syntax | Behavior | Example |
---|---|---|---|
Required Positional | param |
Must be provided, matched by position | def method(name) |
Optional Positional | param = default |
Uses default if not provided | def method(name = "default") |
Splat Arguments | *params |
Captures variable positional args into array | def method(*args) |
Required Keyword | param: |
Must be provided with keyword syntax | def method(name:) |
Optional Keyword | param: default |
Uses default if not provided | def method(name: "default") |
Double Splat | **params |
Captures variable keyword args into hash | def method(**opts) |
Block Parameter | &block |
Captures block as Proc object | def method(&block) |
Parameter Order Rules
Ruby enforces strict parameter ordering in method signatures:
def method_signature(
required_positional, # 1. Required positional parameters
optional_positional = default, # 2. Optional positional parameters
*splat_args, # 3. Splat parameter (captures remaining positional)
required_keyword:, # 4. Required keyword parameters
optional_keyword: default, # 5. Optional keyword parameters
**double_splat, # 6. Double splat (captures remaining keyword args)
&block # 7. Block parameter
)
Method Call Argument Resolution
Call Pattern | Argument Assignment | Notes |
---|---|---|
method(a, b, c) |
Positional assignment left-to-right | Matches required then optional positional params |
method(a, key: value) |
Mixed positional and keyword | Positional args first, then keyword args |
method(*array) |
Array expansion to positional args | Each array element becomes separate argument |
method(**hash) |
Hash expansion to keyword args | Each hash key-value becomes keyword argument |
method(a, *array, **hash) |
Combined expansion | Positional, then array expansion, then hash expansion |
Block Handling Methods
Method | Purpose | Return Value | Example |
---|---|---|---|
block_given? |
Check if block provided | Boolean | if block_given? |
yield |
Execute block with arguments | Block return value | yield(arg1, arg2) |
block.call |
Execute captured block | Block return value | block.call(arg1, arg2) |
block.arity |
Number of block parameters | Integer | block.arity #=> 2 |
block.parameters |
Block parameter information | Array of arrays | block.parameters #=> [[:req, :x], [:opt, :y]] |
Parameter Introspection
def sample_method(req, opt = nil, *args, keyword:, opt_kw: "default", **kwargs, &block)
# Method implementation
end
method = method(:sample_method)
puts method.parameters
# => [[:req, :req], [:opt, :opt], [:rest, :args], [:keyreq, :keyword], [:key, :opt_kw], [:keyrest, :kwargs], [:block, :block]]
Parameter Type Symbols
Symbol | Parameter Type | Description |
---|---|---|
:req |
Required positional | Must provide positional argument |
:opt |
Optional positional | Positional with default value |
:rest |
Splat parameter | Captures remaining positional args |
:keyreq |
Required keyword | Must provide keyword argument |
:key |
Optional keyword | Keyword with default value |
:keyrest |
Double splat | Captures remaining keyword args |
:block |
Block parameter | Captures block as Proc |
Common Error Types
Error Class | Cause | Prevention |
---|---|---|
ArgumentError |
Wrong number of arguments | Validate argument count matches signature |
KeyError |
Missing required keyword argument | Provide all required keyword parameters |
TypeError |
Argument type mismatch | Validate argument types before processing |
NoMethodError |
Method called on nil from default parameter | Use safe navigation or nil checks |
Performance Considerations
Pattern | Performance Impact | Alternative |
---|---|---|
Mutable defaults | Memory shared across calls | Use nil default with ` |
Large splat arrays | Array creation overhead | Limit splat usage or use keyword args |
Block conversion | Proc object creation | Use yield instead of &block.call when possible |
Parameter validation | Method call overhead | Balance validation depth with performance needs |
Deep parameter introspection | Reflection overhead | Cache method signatures when possible |