CrackedRuby logo

CrackedRuby

Numbered Block Parameters (_1, _2)

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}" }