Inconsistency of arity between Hash.each and lambdas
Asked Answered
R

2

3

I lifted the following example from Josh Susser

  def strip_accents params
    thunk = lambda do |key,value|
      case value
        when String then value.remove_accents!
        when Hash   then value.each(&thunk)
      end
    end
    params.each(&thunk)
  end

when I put it in the the rails console (irb), and call it with a hash, I get the following:

ruby-1.9.2-p136 :044 > `ruby --version`
 => "ruby 1.9.2p136 (2010-12-25 revision 30365) [i686-linux]\n"
ruby-1.9.2-p136 :045 > strip_accents({:packs=>{:qty=>1}})
ArgumentError: wrong number of arguments (1 for 2)
        from (irb):32:in `block in strip_accents'
        from (irb):37:in `each'
        from (irb):37:in `strip_accents'
        from (irb):45
        from /longpathtrimedforclarity/console.rb:44:in `start'
        from /longpathtrimedforclarity/console.rb:8:in `start'
        from /longpathtrimedforclarity/commands.rb:23:in `<top (required)>'
        from script/rails:6:in `require'
        from script/rails:6:in `<main>'

I understand that lambdas check arity, but I see two arguments in the lambda definition. If I change lambda do to Proc.new do, The code executes, and I get the expected result.

Josh's example is from 2008, so I'm assuming this is a difference in Ruby 1.8 and 1.9. What's going on here?

Raincoat answered 10/3, 2011 at 16:32 Comment(0)
D
3

Indeed, it appears to have changed between 1.8 and 1.9, but this change fixes it for 1.9.2, at least in my tests:

def strip_accents params
  thunk = lambda do |h|
    key, value = h
    case value
    when String then value.remove_accents!
    when Hash   then value.each(&thunk)
    end
  end
  params.each(&thunk)
end

This approach turns out to be backward-compatible with Ruby 1.8.7, as well.

Delftware answered 10/3, 2011 at 16:56 Comment(0)
F
1

Hash#each, just like every other #each method, yields one argument to the block. In the case of Hash#each, that one argument is a two-element array consisting of the key and the value.

So, Hash#each yields one argument, but your lambda has two mandatory parameters, therefore you get an arity error.

It works with blocks, since blocks are less strict about their arguments, and in particular, if a block has multiple parameters, but only gets one argument, it will try to deconstruct the argument as if it had been passed in with a splat.

There are two kinds of Procs: lambdas and non-lambdas (confusingly, the latter are usually also called Procs). Lambdas behave like methods in terms of how the return keyword behaves and (more importantly, for this case) how they bind arguments, whereas non-lambda Procs behave like blocks in terms of how return and argument binding work. That's why Proc.new (which creates a non-lambda Proc) works, but lambda (which obviously creates a lambda) doesn't.

You can check whether a Proc is a lambda or not by calling Proc#lambda?.

If you want to deconstruct the argument, you will have to do so explicitly, the same way you would when you define a method:

lambda do |(key, value)|

And, yes, a more sane approach to argument binding for blocks, Procs and lambdas was one of the major backwards-incompatible changes in Ruby 1.9.

Fatidic answered 10/3, 2011 at 22:37 Comment(1)
FWIW, the Pickaxe used the term "raw proc" to distinguish between the proc style proc and the lambda style proc. I don't think this is exactly textbook Liskov Substitution Principle behavior, though.Linc

© 2022 - 2024 — McMap. All rights reserved.