The code:
a = [1, 2, 3]
h = {a: 1}
def f args
p args
end
h.map(&method(:f))
a.map(&method(:f))
h.map do |k,v|
p [k,v]
end
The output:
[:a, 1]
1
2
3
[:a, 1]
Why can't I define f for a hash as follows?
def f k, v
p [k, v]
end
You are correct that the reason stems from the one of the two main differences between proc's and lambda's. I'll trying explaining it in a slightly different way than you did.
Consider:
a = [:a, 1]
h = {a: 1}
def f(k,v)
p [k, v]
end
a.each(&method(:f))
#-> in `f': wrong number of arguments (1 for 2) (ArgumentError)
h.each(&method(:f))
#-> in `f': wrong number of arguments (1 for 2) (ArgumentError)
where I use #-> to show what is printed and #=> to show what is returned. You used map, but each is more appropriate here, and makes the same point.
In both cases elements of the receiver are being passed to the block1:
&method(:f)
which is (more-or-less, as I will explain) equivalent to:
{ |k,v| p [k,v] }
The block is complaining (for both the array and hash) that it is expecting two arguments but receiving only one, and that is not acceptable. "Hmmm", the reader is thinking, "why doesn't it disambiguate in the normal way?"
Let's try using the block directly:
a.map { |k,v| p [k,v] }
#-> [:a, nil]
# [1, nil]
h.map { |k,v| p [k,v] }
#-> [:a, 1]
This works as expected, but does not return what we wanted for the array.
The first element of a (:a) is passed into the block and the block variables are assigned:
k,v = :a
#=> :a
k #=> :a
v #=> nil
and
p [k,v]
#-> :a
#-> nil
Next, 1 is passed to the block and [1,nil] is printed.
Let's try one more thing, using a proc created with Proc::new:
fp = Proc.new { |k,v| p [k, v] }
#=> #<Proc:0x007ffd6a0a8b00#(irb):34>
fp.lambda?
#=> false
a.each { |e| fp.call(e) }
#-> [:a, nil]
#-> [:a, 1]
h.each { |e| fp[e] }
#-> [:a, 1]
(Here I've used one of three aliases for Proc#call.) We see that calling the proc has the same result as using a block. The proc expects two arguments and but receives only one, but, unlike the lambda, does not complain2.
This tells us that we need to make small changes to a and f:
a = [[:a, 1]]
h = {a: 1}
def f(*(k,v))
p [k, v]
end
a.each(&method(:f))
#-> [:a, 1]
h.each(&method(:f))
#-> [:a, 1]
Incidentally, I think you may have fooled yourself with the variable name args:
def f args
p args
end
as the method has a single argument regardless of what you call it. :-)
1 The block is created by & calling Method#to_proc on the method f and then converting the proc (actually a lambda) to a block.
2 From the docs for Proc: "For procs created using lambda or ->() an error is generated if the wrong number of parameters are passed to a Proc with multiple parameters. For procs created using Proc.new or Kernel.proc, extra parameters are silently discarded."
As it appears, it must be some sort of implicit destructuring (or non-strict arguments handling), which works for procs, but doesn't for lambdas:
irb(main):007:0> Proc.new { |k,v| p [k,v] }.call([1,2])
[1, 2]
=> [1, 2]
irb(main):009:0> lambda { |k,v| p [k,v] }.call([1,2])
ArgumentError: wrong number of arguments (1 for 2)
from (irb):9:in `block in irb_binding'
from (irb):9:in `call'
from (irb):9
from /home/yuri/.rubies/ruby-2.1.5/bin/irb:11:in `<main>'
But one can make it work:
irb(main):010:0> lambda { |(k,v)| p [k,v] }.call([1,2])
[1, 2]
=> [1, 2]
And therefore:
def f ((k, v))
p [k, v]
end
So Hash#map always passes one argument.
UPD
This implicit destructuring also happens in block arguments.
names = ["Arthur", "Ford", "Trillian"]
ids = [42, 43, 44]
id_names = ids.zip(names) #=> [[42, "Arthur"], [43, "Ford"], [44, "Trillian"]]
id_names.each do |id, name|
puts "user #{id} is #{name}"
end
http://globaldev.co.uk/2013/09/ruby-tips-part-2/
UPD Don't take me wrong. I'm not suggesting writing such code (def f ((k, v))). In the question I was asking for explanation, not for the solution.
Related
So I implemented a custom reduce/inject method as following
And I check what's the value of self early on
def my_inject(arg=nil, &block)
puts self
arg.nil? ? memo = self.shift() : memo = arg
until self.empty?
memo = block.call(memo, self.shift)
end
memo
end
my_array = [3, 5, 8, 9]
#without arguments works great
p my_array.my_inject { |m, v| m + v}
=> [3, 5, 8, 9]
=> 25
#with an argument does not work so great
p my_array.my_inject(100) { |m, v| m + v }
=> []
=> 100
When I call the method on an array I get what I expected only if I don't pass any arguments.
When I pass an argument (here 100) the result of printing self becomes an empty array... and so my 'until' never runs, why is that ?
Thanks in advance for your help!
Let's say I have two enumerators, enum1 and enum2 that must be lazily iterated through (because they have side effects). How do I construct a third enumerator enum3 where enum3.each{|x| x} would lazily return the equivalent of enum1 + enum2?
In my real world use case, I'm streaming in two files, and need to stream out the concatenation.
This seems to work just how I want;
enums.lazy.flat_map{|enum| enum.lazy }
Here's the demonstration. Define these yielding methods with side-effects;
def test_enum
return enum_for __method__ unless block_given?
puts 'hi'
yield 1
puts 'hi again'
yield 2
end
def test_enum2
return enum_for __method__ unless block_given?
puts :a
yield :a
puts :b
yield :b
end
concated_enum = [test_enum, test_enum2].lazy.flat_map{|en| en.lazy }
Then call next on the result, showing that the side effects happen lazily;
[5] pry(main)> concated_enum.next
hi
=> 1
[6] pry(main)> concated_enum.next
hi again
=> 2
Here's some code I wrote for fun awhile back with lazy enumeration thrown in:
def cat(*args)
args = args.to_enum
Enumerator.new do |yielder|
enum = args.next.lazy
loop do
begin
yielder << enum.next
rescue StopIteration
enum = args.next.lazy
end
end
end
end
You would use it like this:
enum1 = [1,2,3]
enum2 = [4,5,6]
enum3 = cat(enum1, enum2)
enum3.each do |n|
puts n
end
# => 1
# 2
# 3
# 4
# 5
# 6
...or just:
cat([1,2,3],[4,5,6]).each {|n| puts n }
Since Ruby 2.6 you can use Enumerable#chain/Enumerator::Chain:
a = [1, 2, 3].lazy
b = [4, 5, 6].lazy
a.chain(b).to_a
# => [1, 2, 3, 4, 5, 6]
Enumerator::Chain.new(a, b).to_a
# => [1, 2, 3, 4, 5, 6]
In Ruby, what does the Array#product method do when given a block? The documentation says "If given a block, product will yield all combinations and return self instead." What does it mean to yield all combinations? What does the method do with the given block?
By "yield all combinations" it means that it will yield (supply) all combinations of elements in the target (self) and other (argument) arrays to the given block.
For example:
a = [1, 2]
b = [:foo, :bar]
a.product(b) { |x| puts x.inspect } # => [1, 2]
# [1, :foo]
# [1, :bar]
# [2, :foo]
# [2, :bar]
It is roughly equivalent to this function:
class Array
def yield_products(other)
self.each do |x|
other.each do |y|
yield [x, y] if block_given?
end
end
end
end
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>'
I need send in method argument which is array and hash simultaneously. But don't know, how. Here is example:
def method(here should be this argument)
end
def show(*a)
p a
if a.length.even? == true
p Hash[*a]
else
p "hash conversion not possible"
end
end
show(1,2,3,4)
show(1,2,3)
output:
[1, 2, 3, 4]
{1=>2, 3=>4}
[1, 2, 3]
"hash conversion not possible"
EDIT:
From comment of OP here is the code OP could use:
def add(*arg)
#entries = {}
arg.each_slice(2) do |a| #entries[a.first] = a.last end
p #entries
end
add(1,2,3,4)
Output:
{1=>2, 3=>4}