Overview
Numbered block parameters provide a concise syntax for accessing positional arguments within Ruby blocks without explicitly declaring parameter names. Ruby automatically assigns the first block argument to _1
, the second to `_2, and continues this pattern for additional parameters. This feature reduces the verbosity of simple block operations while maintaining clarity in parameter access.
Ruby implements numbered parameters through implicit parameter binding during block evaluation. When Ruby encounters _1
, _2
, or similar numbered identifiers within a block, it automatically creates the corresponding parameters and binds them to the block arguments in order. The Ruby parser recognizes these numbered parameters and establishes the proper lexical scoping within the block context.
The feature works with any method that yields to blocks, including iteration methods, collection operations, and custom methods. Numbered parameters integrate seamlessly with Ruby's existing block infrastructure without requiring special method definitions or additional setup.
[1, 2, 3].map { |x| x * 2 }
# Equivalent using numbered parameters
[1, 2, 3].map { _1 * 2 }
# => [2, 4, 6]
hash = { a: 1, b: 2, c: 3 }
hash.map { |key, value| "#{key}: #{value}" }
# Equivalent using numbered parameters
hash.map { "#{_1}: #{_2}" }
# => ["a: 1", "b: 2", "c: 3"]
data = [[1, 'a'], [2, 'b'], [3, 'c']]
data.select { |num, letter| num > 1 && letter < 'c' }
# Equivalent using numbered parameters
data.select { _1 > 1 && _2 < 'c' }
# => [[2, "b"]]
The primary use cases include simple transformations, filtering operations, and reduction scenarios where parameter names add little semantic value. Numbered parameters excel in functional programming patterns and method chaining where conciseness improves readability.
Basic Usage
Numbered parameters work with single-argument blocks by using _1
to reference the first block parameter. This pattern applies to common iteration methods like map
, select
, reject
, and others that yield single values.
numbers = [1, 2, 3, 4, 5]
doubled = numbers.map { _1 * 2 }
# => [2, 4, 6, 8, 10]
words = ['hello', 'world', 'ruby']
lengths = words.map { _1.length }
# => [5, 5, 4]
files = ['readme.txt', 'config.rb', 'test.py']
ruby_files = files.select { _1.end_with?('.rb') }
# => ["config.rb"]
Multi-parameter blocks use _1
, _2
, _3
, and so forth to access parameters in order. Hash iteration commonly uses _1
for keys and _2
for values, while array operations with indexes use _1
for values and _2
for indexes.
person = { name: 'John', age: 30, city: 'Boston' }
descriptions = person.map { "#{_1} is #{_2}" }
# => ["name is John", "age is 30", "city is Boston"]
colors = ['red', 'green', 'blue']
indexed = colors.map.with_index { "#{_2}: #{_1}" }
# => ["0: red", "1: green", "2: blue"]
data = { users: 100, posts: 500, comments: 1200 }
summary = data.map { "#{_1.to_s.capitalize}: #{_2}" }.join(', ')
# => "Users: 100, Posts: 500, Comments: 1200"
Numbered parameters work within nested method calls and complex expressions. Ruby maintains the parameter binding throughout the block scope, allowing access to numbered parameters in nested contexts.
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened_doubled = matrix.flat_map { _1.map { _1 * 2 } }
# Note: Inner _1 refers to individual elements, outer _1 refers to arrays
# => [2, 4, 6, 8, 10, 12, 14, 16, 18]
users = [
{ name: 'Alice', scores: [85, 92, 78] },
{ name: 'Bob', scores: [91, 87, 95] },
{ name: 'Carol', scores: [88, 90, 92] }
]
averages = users.map { { name: _1[:name], avg: _1[:scores].sum.fdiv(_1[:scores].size) } }
# => [{:name=>"Alice", :avg=>85.0}, {:name=>"Bob", :avg=>91.0}, {:name=>"Carol", :avg=>90.0}]
The syntax also works with conditional operations and logical expressions within blocks. Ruby evaluates numbered parameters as regular variables within the block scope.
scores = [85, 92, 67, 78, 94, 88]
high_scores = scores.select { _1 > 85 }
# => [92, 94, 88]
transactions = [
{ type: 'deposit', amount: 500 },
{ type: 'withdrawal', amount: 200 },
{ type: 'deposit', amount: 150 }
]
deposits = transactions.select { _1[:type] == 'deposit' && _1[:amount] > 100 }
# => [{:type=>"deposit", :amount=>500}, {:type=>"deposit", :amount=>150}]
Advanced Usage
Numbered parameters support complex parameter patterns when methods yield multiple values or nested structures. Ruby maintains parameter ordering even with destructuring operations and multiple assignment patterns.
coordinates = [[0, 1], [2, 3], [4, 5]]
distances = coordinates.map { Math.sqrt(_1[0]**2 + _1[1]**2) }
# => [1.0, 3.605551275463989, 6.4031242374328485]
# Using numbered parameters with array destructuring context
people = [['John', 'Doe', 30], ['Jane', 'Smith', 25], ['Bob', 'Johnson', 35]]
formatted = people.map { "#{_1[1]}, #{_1[0]} (#{_1[2]})" }
# => ["Doe, John (30)", "Smith, Jane (25)", "Johnson, Bob (35)"]
Nested blocks require careful attention to parameter scoping. Each block establishes its own numbered parameter scope, with inner blocks shadowing outer numbered parameters of the same number.
grouped_data = {
'fruits' => ['apple', 'banana', 'orange'],
'colors' => ['red', 'yellow', 'blue'],
'numbers' => [1, 2, 3]
}
# Complex nested transformation
result = grouped_data.map do
category_name = _1
items = _2
{
category: category_name,
processed: items.map.with_index { "#{_2}: #{_1.upcase}" },
count: items.size
}
end
# => [
# {:category=>"fruits", :processed=>["0: APPLE", "1: BANANA", "2: ORANGE"], :count=>3},
# {:category=>"colors", :processed=>["0: RED", "1: YELLOW", "2: BLUE"], :count=>3},
# {:category=>"numbers", :processed=>["0: 1", "1: 2", "2: 3"], :count=>3}
# ]
Numbered parameters work with custom methods that yield multiple values or complex objects. The parameter binding follows the same rules regardless of the yielding method's implementation.
def process_in_batches(array, batch_size)
array.each_slice(batch_size) do |batch|
yield batch, array.index(batch.first), batch.size
end
end
data = (1..10).to_a
process_in_batches(data, 3) { puts "Batch: #{_1}, Starting at index: #{_2}, Size: #{_3}" }
# Output:
# Batch: [1, 2, 3], Starting at index: 0, Size: 3
# Batch: [4, 5, 6], Starting at index: 3, Size: 3
# Batch: [7, 8, 9], Starting at index: 6, Size: 3
# Batch: [10], Starting at index: 9, Size: 1
Method chaining with numbered parameters creates powerful functional programming patterns. Each method in the chain can use numbered parameters independently.
sales_data = [
{ quarter: 'Q1', region: 'North', sales: 150000, costs: 120000 },
{ quarter: 'Q1', region: 'South', sales: 200000, costs: 150000 },
{ quarter: 'Q2', region: 'North', sales: 180000, costs: 130000 },
{ quarter: 'Q2', region: 'South', sales: 220000, costs: 160000 }
]
quarterly_profit = sales_data
.group_by { _1[:quarter] }
.map { [_1, _2.sum { _1[:sales] - _1[:costs] }] }
.to_h
# => {"Q1"=>80000, "Q2"=>110000}
# Complex transformation with multiple numbered parameter contexts
regional_analysis = sales_data
.group_by { _1[:region] }
.map do
region = _1
records = _2
{
region: region,
total_profit: records.sum { _1[:sales] - _1[:costs] },
quarters: records.map { _1[:quarter] }.uniq.sort,
avg_margin: records.map { (_1[:sales] - _1[:costs]).fdiv(_1[:sales]) }.sum / records.size
}
end
# => [
# {:region=>"North", :total_profit=>80000, :quarters=>["Q1", "Q2"], :avg_margin=>0.23333333333333334},
# {:region=>"South", :total_profit=>90000, :quarters=>["Q1", "Q2"], :avg_margin=>0.22727272727272727}
# ]
Common Pitfalls
Mixing numbered parameters with explicit parameters in the same block causes a syntax error. Ruby requires consistent parameter declaration within a single block scope.
# This raises SyntaxError
[1, 2, 3].map { |x| _1 + x }
# SyntaxError: ordinary parameter is defined
# Correct approaches
[1, 2, 3].map { |x| x * 2 }
[1, 2, 3].map { _1 * 2 }
Numbered parameter scope isolation in nested blocks creates confusion when developers expect outer block parameters to remain accessible. Each block creates its own numbered parameter namespace.
matrix = [[1, 2], [3, 4], [5, 6]]
# Problematic: expecting outer _1 in inner block
# This doesn't work as expected
result = matrix.map do
row = _1 # _1 refers to the current row
row.map { _1 + row.first } # Inner _1 refers to individual elements, not the row
end
# => [[2, 3], [6, 7], [10, 11]]
# Clear solution using explicit parameters for nested context
result = matrix.map do |row|
row.map { _1 + row.first }
end
# => [[2, 3], [6, 7], [10, 11]]
# Alternative: store outer parameter in variable
result = matrix.map do
current_row = _1
current_row.map { _1 + current_row.first }
end
Numbered parameters beyond _9
raise syntax errors. Ruby only supports numbered parameters from _1
through _9
, limiting this feature to methods yielding at most nine parameters.
# This works
def yield_many_params
yield 1, 2, 3, 4, 5, 6, 7, 8, 9
end
yield_many_params { puts "#{_1}, #{_2}, #{_3}, #{_4}, #{_5}, #{_6}, #{_7}, #{_8}, #{_9}" }
# => "1, 2, 3, 4, 5, 6, 7, 8, 9"
# This raises SyntaxError
# yield_many_params { puts _10 }
# SyntaxError: too many numbered parameters
Readability degrades rapidly with higher-numbered parameters. Code using _4
, _5
, and beyond becomes difficult to understand without context about parameter ordering.
# Poor readability with many numbered parameters
user_data = [['John', 'Doe', 30, 'Engineer', 75000, 'Boston', 'Active', '2020-01-15']]
formatted = user_data.map { "#{_2}, #{_1} - #{_4} in #{_6} (#{_7}) - #{_5}/year since #{_8}" }
# => ["Doe, John - Engineer in Boston (Active) - 75000/year since 2020-01-15"]
# Better approach with explicit parameters for complex data
formatted = user_data.map do |first, last, age, role, salary, city, status, start_date|
"#{last}, #{first} - #{role} in #{city} (#{status}) - #{salary}/year since #{start_date}"
end
Variable shadowing occurs when numbered parameters conflict with local variables named _1
, _2
, etc. Ruby treats these as regular variable references, not numbered parameters.
# Shadowing example
_1 = 'local variable'
[1, 2, 3].map { _1 * 2 }
# This uses the local variable _1, not the block parameter
# => ["local variable", "local variable", "local variable"]
# Solution: avoid local variables with numbered parameter names
first = 'local variable'
[1, 2, 3].map { _1 * 2 }
# => [2, 4, 6]
Performance implications exist when numbered parameters create unnecessary object allocations in tight loops. While the performance difference is minimal, explicit parameters can be slightly more efficient in performance-critical code.
# Benchmark comparison (conceptual)
large_array = (1..1_000_000).to_a
# Numbered parameters
start_time = Time.now
result1 = large_array.map { _1 * 2 }
numbered_time = Time.now - start_time
# Explicit parameters
start_time = Time.now
result2 = large_array.map { |x| x * 2 }
explicit_time = Time.now - start_time
# Difference is typically negligible but measurable
Reference
Syntax Rules
Pattern | Usage | Valid | Description |
---|---|---|---|
_1 |
Single parameter | Yes | First block parameter |
_2 |
Second parameter | Yes | Second block parameter |
_1 through _9 |
Multiple parameters | Yes | Parameters 1-9 supported |
_10 |
Tenth parameter | No | Exceeds maximum limit |
_0 |
Zero parameter | No | Invalid numbered parameter |
Mixed |x| _1 |
Combined syntax | No | Cannot mix explicit and numbered |
Scoping Behavior
Context | Behavior | Example |
---|---|---|
Single block | _1 refers to first parameter |
[1, 2].map { _1 * 2 } |
Nested blocks | Inner block shadows outer | matrix.map { _1.map { _1 + 1 } } |
Local variable _1 |
Shadows numbered parameter | _1 = 5; [1, 2].map { _1 } returns [5, 5] |
Method parameter | No conflict | def foo(_1); [1, 2].map { _1 }; end |
Method Compatibility
Method Type | Compatible | Notes |
---|---|---|
Array#map |
Yes | Most common usage |
Hash#each |
Yes | _1 = key, _2 = value |
Enumerable#select |
Yes | Standard filtering |
Enumerable#reduce |
Yes | _1 = accumulator, _2 = element |
Enumerable#with_index |
Yes | _1 = element, _2 = index |
Custom yield methods | Yes | Works with any yielding method |
Proc objects | Yes | Same scoping rules apply |
Lambda objects | Yes | Same scoping rules apply |
Error Conditions
Condition | Error Type | Message |
---|---|---|
Mixed parameters | SyntaxError |
"ordinary parameter is defined" |
_10 or higher |
SyntaxError |
"too many numbered parameters" |
_0 usage |
SyntaxError |
"invalid parameter name" |
Invalid syntax | SyntaxError |
Various parsing errors |
Best Practices Reference
Scenario | Recommendation | Reasoning |
---|---|---|
1-2 simple parameters | Use numbered parameters | Increased conciseness |
3+ parameters | Consider explicit parameters | Better readability |
Complex logic | Use explicit parameters | Improved maintainability |
Nested blocks | Use explicit parameters | Avoid scoping confusion |
Team codebases | Establish consistent style | Team preference matters |
Library code | Prefer explicit parameters | Better documentation |
Common Patterns
# Transformation patterns
collection.map { _1.some_method }
collection.map { _1 * factor }
collection.map { [_1, _2].join(separator) }
# Filtering patterns
collection.select { _1 > threshold }
collection.reject { _1.nil? }
hash.select { _2 > minimum_value }
# Reduction patterns
collection.reduce { _1 + _2 }
collection.reduce { _1 > _2 ? _1 : _2 }
hash.reduce({}) { _1.merge(_2[0] => _2[1].transform) }
# Iteration patterns
collection.each { process(_1) }
hash.each { puts "#{_1}: #{_2}" }
collection.each.with_index { puts "#{_2}: #{_1}" }