My understanding was that Hash#select
and Hash#reject
each passes an array of key and its value [key, value]
as a single block argument for each iteration, and you can directly pick them separately within the block using implicit destructive assignment:
{a: 1, b: 2}.select{|k, v| k == :a} # => {:a => 1}
{a: 1, b: 2}.reject{|k, v| v == 1} # => {:b => 2}
or explicit destructive assignment:
{a: 1, b: 2}.select{|(k, v)| k == :a} # => {:a => 1}
I expected that, when I pass a unary block, the whole [key, value]
array would be passed, but in reality, it seems like the key is passed:
{a: 1}.select{|e| p e} # => Prints `:a` (I expected `[:a, 1]`)
Why does it work this way? For other Hash
instance methods like map
, the whole [key, value]
array is passed.
If it were especially designed to work differently for unary blocks as compared to binary blocks, then I can understand it is useful. But, then I would not understand why the case above with explicit destructive assignment works as is. And I also do not find any document mentioning such specification.
Edit I had a wrong result for {a: 1, b: 2}.reject{|(k, v)| v == 1}
. It is corrected here:
{a: 1, b: 2}.reject{|(k, v)| v == 1} # => {:a=>1, :b=>2} (not `{:b=>2}`)
Now, this also indicates that (k, v)
is the key
, not [key, value]
, so v
is always nil
. Cf. Darek Nędza's comment.
It's actually passing two arguments, always.
What you're observing is merely the difference between how procs and lambdas treat excess arguments. Blocks (Procs unless you tell Ruby otherwise) behave as if it had an extra splat and discard excess arguments, whereas lambdas (and method objects) reject the caller due to the incorrect arity.
Demonstration:
>> p = proc { |e| p e }
=> #<Proc:0x007f8dfa1c8b50@(irb):1>
>> l = lambda { |e| p e }
=> #<Proc:0x007f8dfa838620@(irb):2 (lambda)>
>> {a: 1}.select &p
:a
=> {:a=>1}
>> {a: 1}.select &l
ArgumentError: wrong number of arguments (2 for 1)
from (irb):2:in `block in irb_binding'
from (irb):4:in `select'
from (irb):4
from /usr/local/bin/irb:11:in `<main>'
As an aside, since it was mentioned in the comments: map
, in contrast, actually passes one argument. It gets allocated to two different variables because you can assign multiple variables with an array on the right side of the assignment operator, but it's really one argument all along.
Demonstration:
>> {a: 1}.map { |k, v| p k, v }
:a
1
>> {a: 1}.map &p
[:a, 1]
=> [[:a, 1]]
>> {a: 1}.map &l
[:a, 1]
And upon changing p
and l
defined further up:
>> p = proc { |k, v| p k, v }
=> #<Proc:0x007ffd94089258@(irb):1>
>> l = lambda { |k, v| p k, v }
=> #<Proc:0x007ffd940783e0@(irb):2 (lambda)>
>> {a: 1}.map &p
:a
1
=> [[:a, 1]]
>> {a: 1}.map &l
ArgumentError: wrong number of arguments (1 for 2)
from (irb):2:in `block in irb_binding'
from (irb):4:in `each'
from (irb):4:in `map'
from (irb):4
from /usr/local/bin/irb:11:in `<main>'
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With