5

How can I pass a lambda to hash.each, so I can re-use some code?

> h = { a: 'b' }
> h.each do |key, value| end
 => {:a=>"b"}
> test = lambda do |key, value| puts "#{key} = #{value}" end 
> test.call('a','b')
a = b
> h.each &test
ArgumentError: wrong number of arguments (1 for 2)
  from (irb):1:in `block in irb_binding'
  from (irb):5:in `each'
  from (irb):5
  from /Users/jstillwell/.rvm/rubies/ruby-1.9.3-p362/bin/irb:16:in `<main>'
> h.each test
ArgumentError: wrong number of arguments(1 for 0)
  from (irb):8:in `each'
  from (irb):8
  from /Users/jstillwell/.rvm/rubies/ruby-1.9.3-p362/bin/irb:16:in `<main>'
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
DragonFax
  • 4,625
  • 2
  • 32
  • 35

3 Answers3

12

each yields the current element to the block, ergo the lambda needs to take one argument, not two. In case of a Hash the element being yielded is a two-element Array with the first element being the key and the second element being the value.

test = lambda do |el| puts "#{el.first} = #{el.last}" end
h.each &test
# a = b

Alternatively, you can use Ruby's support for destructuring bind in parameter lists:

test = lambda do |(k, v)| puts "#{k} = #{v}" end
h.each &test
# a = b

Or, instead of using a lambda which has the same argument binding semantics as a method, you could use a Proc, which has the same argument binding semantics as a block, i.e. it will unpack a single Array argument into multiple parameter bindings:

test = proc do |k, v| puts "#{k} = #{v}" end
h.each &test
# a = b
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • The issue I have (after I found the answer) is that, as you can see on my second line of code, `each` clearly takes a block with 2 arguments. It just fails to take a `lambda` with 2 arguments. WTF – DragonFax Feb 21 '13 at 00:35
  • `each` *always* yields a *single* argument. *Always*. No exceptions. If you pass a *single* `Array` to a block which takes *multiple* parameters, then the `Array` will be unpacked into the parameters similarly to parallel assignment. But that doesn't have anything to do with `each`, that's just basic Ruby 101 parameter passing. – Jörg W Mittag Feb 21 '13 at 00:39
  • 1
    There's the magic. yield/block unpacks a **single Array** if possible. I couldn't find this documented anywhere, and didn't suspect it. Thanks Jörg. – DragonFax Feb 21 '13 at 00:40
  • 2
    Yes, and `Proc`s have the same parameter passing semantics as blocks (lenient arity checking, array unpacking), whereas `lambda`s have the same parameter passing semantics as methods (strict arity checking, no unpacking). – Jörg W Mittag Feb 21 '13 at 00:41
1

I still feel this is inconsistent syntax, I found the answer (but no apology) in another question Inconsistency of arity between Hash.each and lambdas

I switched it to

lambda do |(key, value)|

then I can pass in

hash.each &test

or I can call it directly with

test.call([key, value])

If someone has a better answer, or at least a succinct excuse why this is necessary. I'll gladly give them the points.

Community
  • 1
  • 1
DragonFax
  • 4,625
  • 2
  • 32
  • 35
  • You needed this transformation because `h.each` was passing each pair as a single object, an array. The array was passed to your `key` param and you were missing one for `value`. With the `(key, value)` notation, it passes the array's value correctly to each of them. Like when you write `a, b = [1, 2]` – oldergod Feb 21 '13 at 00:32
  • I'm not sure why you feel that you need an "excuse" here. `each` *always* yields a *single* argument. Your lambda takes *two* arguments. Obviously, that cannot work. – Jörg W Mittag Feb 21 '13 at 00:33
1

I have come across a similar case where I wanted not to print but to return a key-value pair after doing some operation with the value... the above solution applies when you just want to print but it can become tricky when you need to return a Hash of those key-value pairs. so I thought this could be helpful

I used it to solve the caesar-cipher puzzle

hash1 = {"a" => 1, "b" => 2, "c" => 3}
test = lambda {|k,v| true ? {k => v+2} : {k => v+1} }

after mapping this to an hash it will return an array of Hashes (here is where things get interesting)

hash2 = hash1.map(&test)   # [{"a"=>3}, {"b"=>4}, {"c"=>5}]

we can convert it to hash rubystically!

hash2.inject(:merge!)   # {"a" => 3, "b" => 4, "c" => 5}

Bingo!!! right?