Home > OS >  Can I deconstruct a hash in block parameters in Ruby 3.1, like `|foo:|`?
Can I deconstruct a hash in block parameters in Ruby 3.1, like `|foo:|`?

Time:08-05

In Ruby 2.7, I can effectively deconstruct a hash in block parameters:

>> RUBY_VERSION
=> "2.7.6"
>> [{foo: 123}].each { |foo:| p foo }
123
=> [{:foo=>123}]

In Ruby 3.1, I can't:

>> RUBY_VERSION
=> "3.1.2"
>> [{foo: 123}].each { |foo:| p foo }
(irb):7:in `block in <top (required)>': missing keyword: :foo (ArgumentError)

It is possible to pattern match it outside the parameter list:

[{foo: 123}].each { |x| x => {foo:}; p foo }

But I'm after something in the parameter list, if possible.

CodePudding user response:

Short answer: no

each just passes the {foo: 123} hash as the block argument, but Ruby 3 expects an actual foo: keyword to be passed. It's equivalent to:

def m(foo:)
  p foo
end

m({foo: 123})
# ArgumentError: wrong number of arguments (given 1, expected 0; required keyword: foo)

You have to either pass keyword arguments or convert the hash via **:

m(foo: 123)
# 123

m(**{foo: 123})
# 123

In either way, this has to be "fixed" on the caller's side.

In your code, the caller is each. To get your code working, you could define your own each variant which does the required conversion:

module Enumerable
  def each_keyword
    each { |hash| yield **hash }
  end
end

Which works as expected for the example data:

[{foo: 123}].each_keyword { |foo:| p foo }
# 123

However, I would just stick to each and fetch the value from the passed hash:

[{foo: 123}].each { |h| p h[:foo] }
# 123

If you have a hash, you should probably treat is as one.

CodePudding user response:

Difference in keyword argument handling:

# 2.7
>> [{foo: 123}].each {|*args, **kwargs| puts "args=#{args}, kwargs=#{kwargs}" }
args=[], kwargs={:foo=>123}

# 3.1
>> [{foo: 123}].each {|*args, **kwargs| puts "args=#{args}, kwargs=#{kwargs}" }
args=[{:foo=>123}], kwargs={}
# NOTE: no kwargs, no match.

Closest I could get:

>> [{foo: 1},{foo: 2}].each { _1 => foo:; p foo }
1
2

# in the parameter list
>> [{foo: 1},{foo: 2}].each { |i, foo: i[:foo]| p foo }
1
2
  •  Tags:  
  • ruby
  • Related