Why Does Hash#Select and Hash#Reject Pass a Key to a Unary Block

Why does Hash#select and Hash#reject pass a key to a unary block?

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>'

Should you pass in block variables even if they aren't used?

This is probably opinionated but the rule in the projects I work on is to pass it in but only as a _value or _ as it communicates at the definition of the block that the variable was not forgotten but will not be used.

So in your example this would mean

h.keep_if {|key, _| key % 2 == 0}

We usually enforce it via rubocop using RuboCop::Cop::Lint::UnusedMethodArgument

That same rule is applied when a method is overridden and the overriding implementation does not make use of a variable but needs to list it in order to match the original signature.

Can I pass an array and a block as a parameter?

You can do it by passing the block outside the parentheses (adjacent to the method call):

p mapper([1, 2, 3, 4]) { |index| index * 2 }
# [2, 4, 6, 8]

Otherwise it'll result in a syntax error. Ruby won't know where the block is being passed.

As a side note, you can also define only the array as a needed argument, and then yield the block being passed:

def mapper(arr)
arr.size.times.map do |i|
yield(arr[i])
end
end

p mapper([1, 2, 3, 4]) { |index| index * 2 }
# [2, 4, 6, 8]

Block only works if it is the second parameter

Consider that you can pass an unnamed block, which is very commonly done in Ruby.

adder { 41 }

And you can remove the &block argument altogether.

Named blocks must be the last argument.

All methods can take a block. Most simply do not do anything with a block.

Documentation on Methods from ruby-doc.org for Ruby 2.2.0 states:

There are three types of arguments when sending a message, the
positional arguments, keyword (or named) arguments and the block
argument.

Why does map-each preserve the last value for references to the word to set?

Just like 'FOREACH, 'MAP-EACH binds the block you give it within a context it creates and executes it there.

the X is never created globally. the fact that you gave it a word (and not a lit-word) as an argument is managed by the function's interface which probably uses the lit-word notation, and uses the word given, as-is, un-evaluated, instead of the value it may contain.

for this reason, the X used in your call to map-each doesn't trigger a

** Script error: x has no value

since map-each is grabbing it before it gets evaluated and only uses the word as a token, directly.

To illustrate how binding works more vividly and to show how 'X may survive existence past its original context, here is an example which illustrates the foundation of how words are bound in Rebol (and the fact that this binding persists).

look at this example:

a: context [x: "this"]  
b: context [x: "is"]
c: context [x: "sensational!"]

>> blk: reduce [in a 'x in b 'x in c 'x]
== [x x x]

x: "doh!"
== "doh!"

>> probe reduce blk
["this" "is" "sensational!"]

We created a single block with three 'X words, but none of them are bound to the same context.

Because the binding in Rebol is static, and scope doesn't exist, the same word can have different values, even when they are being manipulated in the same context (in this case the console is the global | user context).

This is the penultimate example of why a word is really NOT a variable in Rebol.

Is it possible to decompose a method that takes an implicit block?

Actually I realized how to do this right after posting my question. The answer is to call the sub-methods with a block with a parameter and yield the parameter:

def each
submethod1 {|out| yield out}
submethod2 {|out| yield out}
yield "whatever"
end

def submethod1
yield "submethod1"
end

def submethod2
yield "submethod2"
end

Iterate over array of arrays

Why does ruby behave this way?

It's because what actually happens internally, when each and other iterators are used with a block instead of a lambda, is actually closer to this:

do |key, value, *rest|
puts key
end

Consider this code to illustrate:

p = proc do |key,value|
puts key
end
l = lambda do |key,value|
puts key
end

Using the above, the following will set (key, value) to (13, 14) and (22, 23) respectively, and the above-mentioned *rest as [16, 11] in the first case (with rest getting discarded):

[[13,14,16,11],[22,23]].each(&p)

In contrast, the following will spit an argument error, because the lambda (which is similar to a block except when it comes to arity considerations) will receive the full array as an argument (without any *rest as above, since the number of arguments is strictly enforced):

[[13,14,16,11],[22,23]].each(&l) # wrong number of arguments (1 for 2)

To get the index in your case, you'll want each_with_index as highlighted in the other answers.

Related discussions:

  • Proc.arity vs Lambda.arity
  • Why does Hash#select and Hash#reject pass a key to a unary block?


Related Topics



Leave a reply



Submit