Overview
Ruby splat operators provide mechanisms for expanding, collecting, and manipulating arrays and hashes in method calls and definitions. The single splat (*) operator works with arrays and positional arguments, while the double splat (**) operator handles hashes and keyword arguments.
The splat operator appears in four primary contexts: method arguments, method parameters, array literals, and assignment operations. Ruby implements these operators through internal conversion protocols, calling to_a
for splat operations and to_hash
for double splat operations.
# Array expansion in method calls
numbers = [1, 2, 3]
puts(*numbers)
# => 1
# => 2
# => 3
# Parameter collection in method definitions
def collect_args(*args)
args
end
collect_args(1, 2, 3) # => [1, 2, 3]
# Hash expansion with double splat
options = { color: 'red', size: 'large' }
def create_item(name, **opts)
{ name: name }.merge(opts)
end
create_item('shirt', **options) # => { name: 'shirt', color: 'red', size: 'large' }
Ruby converts objects using to_a
and to_hash
methods when applying splat operators. The conversion happens automatically, allowing custom objects to participate in splat operations by implementing these methods.
Basic Usage
Array Expansion with Splat
The splat operator expands arrays into individual elements during method calls. Ruby unpacks the array elements as separate arguments to the receiving method.
def greet(first, second, third)
"Hello #{first}, #{second}, and #{third}"
end
names = ['Alice', 'Bob', 'Charlie']
greet(*names) # => "Hello Alice, Bob, and Charlie"
# Without splat - passes array as single argument
# greet(names) # ArgumentError: wrong number of arguments
# Combining regular arguments with splat
greet('David', *['Eve', 'Frank']) # => "Hello David, Eve, and Frank"
Parameter Collection with Splat
Methods use splat parameters to collect variable numbers of arguments into arrays. The splat parameter captures all remaining positional arguments after required parameters.
def log_message(level, *messages)
puts "#{level.upcase}: #{messages.join(' ')}"
end
log_message('info', 'User', 'logged', 'in') # => "INFO: User logged in"
log_message('error', 'Database connection failed') # => "ERROR: Database connection failed"
# Splat parameter with no arguments becomes empty array
log_message('debug') # => "DEBUG: "
Hash Expansion with Double Splat
Double splat operators expand hashes into keyword arguments or collect keyword arguments into hashes. Ruby matches hash keys to method parameter names.
def configure_server(host:, port:, ssl: false)
"Server: #{host}:#{port} (SSL: #{ssl})"
end
config = { host: 'localhost', port: 3000, ssl: true }
configure_server(**config) # => "Server: localhost:3000 (SSL: true)"
# Partial expansion with additional keywords
base_config = { host: 'localhost' }
configure_server(**base_config, port: 8080) # => "Server: localhost:8080 (SSL: false)"
Double Splat Parameter Collection
Methods collect keyword arguments into hashes using double splat parameters. The parameter captures unmatched keyword arguments as key-value pairs.
def create_user(name, email, **attributes)
user = { name: name, email: email }
user.merge(attributes)
end
create_user('John', 'john@example.com', age: 30, role: 'admin')
# => { name: 'John', email: 'john@example.com', age: 30, role: 'admin' }
Advanced Usage
Complex Parameter Patterns
Ruby supports sophisticated parameter combinations mixing required, optional, splat, and keyword parameters. The order follows specific rules: required, optional, splat, keyword, double splat, block.
def complex_method(required, optional = 'default', *args, keyword:, other: 'other', **opts, &block)
{
required: required,
optional: optional,
args: args,
keyword: keyword,
other: other,
opts: opts,
block: block_given?
}
end
result = complex_method('req', 'opt', 1, 2, 3, keyword: 'key', extra: 'value') do
'block content'
end
# => {
# required: 'req',
# optional: 'opt',
# args: [1, 2, 3],
# keyword: 'key',
# other: 'other',
# opts: { extra: 'value' },
# block: true
# }
Array Construction and Decomposition
Splat operators create arrays by collecting elements from various sources. Ruby flattens nested structures when building arrays with splat operations.
# Array construction with multiple splats
first_group = [1, 2]
second_group = [3, 4]
third_group = [5, 6]
combined = [0, *first_group, *second_group, *third_group, 7]
# => [0, 1, 2, 3, 4, 5, 6, 7]
# Conditional array construction
def build_query(base, *conditions, debug: false)
query = [base]
query.concat(conditions) if conditions.any?
query << 'EXPLAIN' if debug
query.join(' ')
end
build_query('SELECT *', 'FROM users', 'WHERE active = 1', debug: true)
# => "SELECT * FROM users WHERE active = 1 EXPLAIN"
Method Forwarding and Delegation
Splat operators enable method delegation by capturing and forwarding arguments without knowing their structure. This pattern supports proxy objects and decorators.
class LoggingWrapper
def initialize(target)
@target = target
end
def method_missing(method, *args, **kwargs, &block)
puts "Calling #{method} with args: #{args.inspect}, kwargs: #{kwargs.inspect}"
result = @target.public_send(method, *args, **kwargs, &block)
puts "Result: #{result.inspect}"
result
end
def respond_to_missing?(method, include_private = false)
@target.respond_to?(method, include_private)
end
end
calculator = LoggingWrapper.new([1, 2, 3, 4, 5])
calculator.sum # Forwards to Array#sum with logging
# Output:
# Calling sum with args: [], kwargs: {}
# Result: 15
Metaprogramming with Dynamic Arguments
Splat operators support dynamic method definition where argument structures change at runtime. Ruby evaluates splat expressions during method calls, enabling flexible APIs.
class DynamicQuery
def self.define_finder(name, *default_conditions, **default_options)
define_method(name) do |*conditions, **options|
all_conditions = [*default_conditions, *conditions]
all_options = default_options.merge(options)
execute_query(all_conditions, all_options)
end
end
define_finder(:find_active_users, 'active = 1', limit: 10)
define_finder(:find_recent_posts, 'created_at > ?', order: 'created_at DESC')
private
def execute_query(conditions, options)
"SELECT * WHERE #{conditions.join(' AND ')} #{format_options(options)}"
end
def format_options(options)
options.map { |k, v| "#{k.upcase} #{v}" }.join(' ')
end
end
query = DynamicQuery.new
query.find_active_users('role = "admin"', limit: 5)
# => "SELECT * WHERE active = 1 AND role = \"admin\" LIMIT 5"
Common Pitfalls
Splat Operator Precedence and Parsing
Ruby parsing rules create unexpected behavior when splat operators interact with other syntax elements. The precedence of splat operations affects how Ruby interprets complex expressions.
# Splat with ranges requires parentheses
# *1..5 # SyntaxError: unexpected '..'
*(1..5) # => [1, 2, 3, 4, 5]
# Method call ambiguity with splat
def test(*args)
args
end
array = [1, 2, 3]
# These are different:
test *array # => [1, 2, 3] - splat expansion
test(*array) # => [1, 2, 3] - same result, clearer syntax
test(array) # => [[1, 2, 3]] - array as single argument
# Splat in array literals vs method calls behaves differently
*array # SyntaxError outside of array context
[*array] # => [1, 2, 3] - valid in array literal
Double Splat Key Conversion
Ruby converts symbol and string keys inconsistently when using double splat operators. Hash key types affect method parameter matching and can cause silent failures.
def process_data(name:, age:)
"#{name} is #{age} years old"
end
# Symbol keys work as expected
symbol_hash = { name: 'Alice', age: 30 }
process_data(**symbol_hash) # => "Alice is 30 years old"
# String keys don't match symbol parameters
string_hash = { 'name' => 'Bob', 'age' => 25 }
# process_data(**string_hash) # ArgumentError: missing keywords: name, age
# Mixed key types create confusion
mixed_hash = { name: 'Charlie', 'age' => 35 }
# process_data(**mixed_hash) # ArgumentError: missing keyword: age
# Solution: convert keys consistently
def symbolize_keys(hash)
hash.transform_keys(&:to_sym)
end
process_data(**symbolize_keys(string_hash)) # => "Bob is 25 years old"
Nil and Empty Collection Behavior
Ruby handles nil values and empty collections differently in splat operations. Unexpected nil values can cause errors or produce empty results.
def safe_expand(*items)
items
end
# Nil splat raises TypeError
nil_array = nil
# safe_expand(*nil_array) # TypeError: no implicit conversion of NilClass into Array
# Empty array produces no arguments
empty_array = []
safe_expand(*empty_array) # => []
# Nil in double splat context
nil_hash = nil
def accept_keywords(**kwargs)
kwargs
end
# accept_keywords(**nil_hash) # TypeError: no implicit conversion of NilClass into Hash
# Safe nil handling patterns
def safe_splat_expand(array)
safe_expand(*(array || []))
end
def safe_double_splat_expand(hash)
accept_keywords(**(hash || {}))
end
safe_splat_expand(nil) # => []
safe_double_splat_expand(nil) # => {}
Performance Impact of Excessive Splatting
Splat operations create intermediate arrays and hashes, causing memory allocation and performance overhead in tight loops or recursive operations.
# Inefficient: creates new arrays on each recursive call
def inefficient_flatten(array, *accumulated)
return accumulated if array.empty?
first, *rest = array
if first.is_a?(Array)
inefficient_flatten(first, *accumulated) + inefficient_flatten(rest, *accumulated)
else
inefficient_flatten(rest, *accumulated, first)
end
end
# More efficient: mutate existing array
def efficient_flatten(array, accumulated = [])
array.each do |element|
if element.is_a?(Array)
efficient_flatten(element, accumulated)
else
accumulated << element
end
end
accumulated
end
# Benchmarking reveals performance difference
nested = [[1, 2], [3, [4, 5]], 6]
# inefficient_flatten(nested) creates many temporary arrays
# efficient_flatten(nested) reuses single accumulator array
Assignment Splat Gotchas
Ruby assignment with splat operators follows specific precedence rules that can produce unexpected results when combining with other assignment patterns.
# Multiple assignment with splat precedence
a, *b, c = [1, 2, 3, 4, 5]
# a = 1, b = [2, 3, 4], c = 5
# Nested assignment splat behavior
(a, *b), c = [[1, 2, 3], 4]
# a = 1, b = [2, 3], c = 4
# Empty splat assignment
*empty = []
# empty = [] - splat variable always becomes array
# Single element splat assignment creates array
*single = [42]
# single = [42] - not 42
# Splat assignment with insufficient elements
x, *y, z = [1]
# x = 1, y = [], z = nil - splat gets empty array, remaining gets nil
Reference
Splat Operator Contexts
Context | Syntax | Purpose | Example |
---|---|---|---|
Method call argument | method(*array) |
Expand array into arguments | puts(*[1, 2, 3]) |
Method definition parameter | def method(*args) |
Collect arguments into array | def log(*messages) |
Array literal | [*array] |
Expand array elements | [0, *[1, 2], 3] |
Assignment target | a, *b = array |
Collect remaining elements | first, *rest = [1, 2, 3] |
Double Splat Operator Contexts
Context | Syntax | Purpose | Example |
---|---|---|---|
Method call argument | method(**hash) |
Expand hash into keyword arguments | create(**options) |
Method definition parameter | def method(**kwargs) |
Collect keyword arguments | def configure(**opts) |
Hash literal | { **hash } |
Merge hash contents | { a: 1, **other } |
Parameter Order Rules
Method definitions must follow this parameter order:
- Required positional parameters
- Optional positional parameters (with defaults)
- Splat parameter (
*args
) - Required keyword parameters
- Optional keyword parameters (with defaults)
- Double splat parameter (
**kwargs
) - Block parameter (
&block
)
def full_signature(req, opt = 'default', *args, req_kw:, opt_kw: 'default', **kwargs, &block)
# All parameter types in correct order
end
Conversion Methods
Operator | Method Called | Fallback Behavior |
---|---|---|
*object |
object.to_a |
TypeError if not array-like |
**object |
object.to_hash |
TypeError if not hash-like |
Common Error Types
Error | Cause | Solution |
---|---|---|
ArgumentError: wrong number of arguments |
Splat expansion mismatch | Check array size vs parameter count |
TypeError: no implicit conversion into Array |
Splat on non-array object | Ensure object responds to to_a |
TypeError: no implicit conversion into Hash |
Double splat on non-hash object | Ensure object responds to to_hash |
ArgumentError: missing keyword |
Hash key doesn't match parameter name | Verify key names match parameter names |
Assignment Splat Patterns
Pattern | Result | Description |
---|---|---|
a, *b = [1, 2, 3] |
a = 1, b = [2, 3] |
Collect remaining elements |
*a, b = [1, 2, 3] |
a = [1, 2], b = 3 |
Collect leading elements |
a, *b, c = [1, 2, 3] |
a = 1, b = [2], c = 3 |
Collect middle elements |
*a = [1, 2, 3] |
a = [1, 2, 3] |
Collect all elements into array |
a, * = [1, 2, 3] |
a = 1 |
Ignore remaining elements |
Method Definition Combinations
Signature | Call Example | Args Result | Kwargs Result |
---|---|---|---|
def m(*args) |
m(1, 2, 3) |
[1, 2, 3] |
N/A |
def m(**kwargs) |
m(a: 1, b: 2) |
N/A | {a: 1, b: 2} |
def m(*args, **kwargs) |
m(1, 2, a: 3) |
[1, 2] |
{a: 3} |
def m(req, *args, **kwargs) |
m(1, 2, 3, a: 4) |
[2, 3] |
{a: 4} |
Performance Considerations
Operation | Performance Impact | Alternative |
---|---|---|
*large_array in method calls |
Creates argument list copy | Pass array directly when possible |
**large_hash in method calls |
Creates keyword argument copy | Use hash parameter |
Recursive splat operations | Cumulative memory allocation | Use iterative approaches |
Nested splat in loops | Repeated array creation | Extract splat outside loop |