5

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.

sawa
  • 165,429
  • 45
  • 277
  • 381
  • 1
    The implementation probably checks the `block.arity` and only yields the right number of arguments. But _why_ that's the implementation is an interesting question. – Alex Wayne Dec 31 '13 at 18:09
  • @AlexWayne I understand it is useful if it is intended, but was not even sure if it was especially designed like this. I could not find any documentation mentioning it. – sawa Dec 31 '13 at 18:12
  • 2
    `b = {a: 1}; b.method(:select) # => #; b.method(:map) # => #` might explain a bit more, even though I feel you know more here – bjhaid Dec 31 '13 at 18:16
  • @bjhaid it's because `puts e` returns nil, and make `select` returns empty hash, while e is not false value, hence returns `{:a=>1}` – Darek Nędza Dec 31 '13 at 18:33
  • 1
    Enumerable#select behaves differently `b = {a: 1}; b.select { |x| p x } # => :a; b.to_enum.select { |x| p x } # => [:a, 1]` – bjhaid Dec 31 '13 at 18:35
  • @DarekNędza deleted that comment as I realized it is not relevant in the context of the discussion – bjhaid Dec 31 '13 at 18:36
  • 3
    Hash implements `select` on its own but leaves `map` to Enumerable (why? that's a mystery). `Hash#select` [explicitly passes two arguments to the block](https://github.com/ruby/ruby/blob/v2_1_0/hash.c#L1195) because it knows about the special k/v structure of a Hash. `Enumerable#map` uses `each` which only knows about single values. Why are some Enumerable methods specially implemented in Hash but some are left to Enumerable is a mystery that would probably be better answered on one of the Ruby-specific forums. – mu is too short Dec 31 '13 at 18:43
  • 1
    This might be helpful: `{a: 1, b: 2}.select{|(k, v)| puts v.class; k == :a} #NilClass`. – Darek Nędza Dec 31 '13 at 18:54
  • @DarekNędza That would solve my concern, but only half-way. It wouldn't work for my `{a: 1, b: 2}.reject{|(k, v)| v == 1}` case. Is `v` `nil` in this case as well? – sawa Dec 31 '13 at 18:56
  • @DarekNędza Sorry, my comment above, as well as a result in my question was wrong. I edited the question. Your hint works all the way. It makes sense. – sawa Dec 31 '13 at 19:03
  • I don't understand your last edit. The deleted line appears to be identical to what you said is now correct. – Peter Alfvin Dec 31 '13 at 19:14
  • @PeterAlfvin I had two mistakes. For `{a: 1, b: 2}.reject{|k, v| v == 1}`, I wrote `# => {:a => 1}`, but I corrected to `# => {:b => 2}`. For `{a: 1, b: 2}.reject{|(k, v)| v == 1}`, I wrote `# => {:a => 1}` as well, but would have intended to be the same as above, which is `# => {:b => 2}`, but actually it was `{:a=>1, :b=>2}`. So `()` makes difference, and the whole `(k, v)` takes only the key, which is assigned to `k`, and `v` becomes `nil` like Darek Nędza suggests. – sawa Dec 31 '13 at 19:26

1 Answers1

8

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>'
Denis de Bernardy
  • 75,850
  • 13
  • 131
  • 154