Search code examples
rubyhashenumerable

Hash Enumerable methods: Inconsistent behavior when passing only one parameter


Ruby's enumerable methods for Hash expect 2 parameters, one for the key and one for the value:

hash.each { |key, value| ... }

However, I notice that the behavior is inconsistent among the enumerable methods when you only pass one parameter:

student_ages = {
"Jack" => 10,
"Jill" => 12,
}

student_ages.each { |single_param| puts "param: #{single_param}" }
student_ages.map { |single_param| puts "param: #{single_param}" }
student_ages.select { |single_param| puts "param: #{single_param}" }
student_ages.reject { |single_param| puts "param: #{single_param}" }

# results:

each...
param: ["Jack", 10]
param: ["Jill", 12]

map...
param: ["Jack", 10]
param: ["Jill", 12]

select...
param: Jack
param: Jill

reject...
param: Jack
param: Jill

As you can see, for each and map, the single parameter gets assigned to a [key, value] array, but for select and reject, the parameter is only the key.

Is there a particular reason for this behavior? The docs don't seem to mention this at all; all of the examples given just assume that you are passing in two parameters.


Solution

  • Just checked Rubinius behavior and it is indeed consistent with CRuby. So looking at the Ruby implementation - it is indeed because #select yields two values:

    yield(item.key, item.value)
    

    while #each yields an array with two values:

    yield [item.key, item.value]
    

    Yielding two values to a block that expects one takes the first argument and ignores the second one:

    def foo
      yield :bar, :baz
    end
    
    foo { |x| p x } # => :bar
    

    Yielding an array will either get completely assigned if the block has one parameter or get unpacked and assigned to each individual value (as if you passed them one by one) if there are two or more parameters.

    def foo
      yield [:bar, :baz]
    end
    
    foo { |x| p x } # => [:bar, :baz]
    

    As for why they made that descision - there probably isn't any good reason behind it, it just wasn't expected people to call them with one argument.