Misc #18609
closedkeyword decomposition in enumerable (question/guidance)
Description
There is a pattern that I have used somewhat often in ruby 2, decomposing hash keys as keyword arguments to blocks for Enumerable, which no longer works in ruby 3. I'm wondering if there is a better way that I am missing (I couldn't find any discussion of this particular thing searching this tracker).
drafts = [
  {name: 'draft4', mod: :Draft04, image: 'draft4.png'},
  {name: 'draft6', mod: :Draft06, image: 'draft6.jpg'},
]
# ruby 2
drafts.each do |name: , mod: , image: |
  ...
# ruby 3
drafts.each do |draft|
  name = draft[:name]
  mod = draft[:mod]
  image = draft[:image]
  ...
the latter is much more cumbersome, but seems necessary with the switch in keyword argument handling in ruby 3.
I can refactor to name, mod, image = draft[:name], draft[:mod], draft[:image]. that is a little better but still more verbose and repetitive than it used to be, and with more keys the line gets very long.
I am expecting this pattern is just a casualty of the keyword split that I will have to rewrite and this issue can be closed, but hoping there may be some better option I have missed.
        
          
          Updated by jeremyevans0 (Jeremy Evans) over 3 years ago
          
          
        
        
      
      If you don't want to change too much code, you can define your own method like this if you want to automatically keyword splat:
module Enumerable
  def each_kw
    each{|v| yield(**v)}
  end
end
drafts.each_kw do |name: , mod: , image: | 
 # ...
end
        
          
          Updated by sawa (Tsuyoshi Sawada) over 3 years ago
          
          
        
        
      
      Perhaps, you can also do this:
drafts.each do |draft|
  name, mod, image = draft.values_at(:name, :mod, :image)
  ...
or if you are sure about the order of the keys, even this (although fragile):
drafts.each do |draft|
  name, mod, image = draft.values
  ...
        
          
          Updated by Hanmac (Hans Mackowiak) over 3 years ago
          
          
        
        
      
      interesting, it seems to be changed in between "3.1.0" and "3.1.1"
        
          
          Updated by baweaver (Brandon Weaver) over 3 years ago
          
          
        
        
      
      sawa (Tsuyoshi Sawada) wrote in #note-2:
Perhaps, you can also do this:
drafts.each do |draft| name, mod, image = draft.values_at(:name, :mod, :image) ...or if you are sure about the order of the keys, even this (although fragile):
drafts.each do |draft| name, mod, image = draft.values ...
If we want to be really interesting we can take a note from pattern matching on those:
drafts.each do |name:, mod: { id:, creator: }, image: { url: }|
  # ...
end
But that's pretty overkill and Whitequark / parser creators would hate us for it.
Anyways, if it's a regression from 3.1.0 to 3.1.1 should probably be patched back, as that's been something I've used myself in the past too.
        
          
          Updated by Eregon (Benoit Daloze) over 3 years ago
          
          
        
        
      
      - Status changed from Open to Closed
 
Hanmac (Hans Mackowiak) wrote in #note-3:
interesting, it seems to be changed in between "3.1.0" and "3.1.1"
This is not true, AFAIK this behavior on purpose since Ruby 3.0.0.
Also please don't claim something like that without a concrete example and result.
Concretely:
$ ruby -ve '[{a: 1}].each { |a:| p a }'
ruby 2.7.5p203 (2021-11-24 revision f69aeb8314) [x86_64-linux]
-e:1: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
-e:1: warning: The called method is defined here
1
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux]
-e:1:in `block in <main>': missing keyword: :a (ArgumentError)
ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [x86_64-linux]
-e:1:in `block in <main>': missing keyword: :a (ArgumentError)
ruby 3.1.1p18 (2022-02-18 revision 53f5fc4236) [x86_64-linux]
-e:1:in `block in <main>': missing keyword: :a (ArgumentError)
ruby 3.2.0dev (2022-03-03T08:56:31Z master c1790f8c11) [x86_64-linux]
-e:1:in `block in <main>': missing keyword: :a (ArgumentError)
What @jeremyevans0 (Jeremy Evans) said is the way, let's close.
        
          
          Updated by Eregon (Benoit Daloze) over 3 years ago
          
          
        
        
      
      To clarify the example does not pass keyword arguments to a block (same for a method) requiring keyword arguments, that's always going to fail -- by design -- on Ruby 3+.
        
          
          Updated by Hanmac (Hans Mackowiak) over 3 years ago
          
          
        
        
      
      Eregon (Benoit Daloze) wrote in #note-5:
Hanmac (Hans Mackowiak) wrote in #note-3:
interesting, it seems to be changed in between "3.1.0" and "3.1.1"
This is not true, AFAIK this behavior on purpose since Ruby 3.0.0.
Also please don't claim something like that without a concrete example and result.
I was testing this on https://try.ruby-lang.org/ because i don't have more ruby versions installed right now, and this did work:
p RUBY_VERSION
drafts = [
  {name: 'draft4', mod: :Draft04, image: 'draft4.png'},
  {name: 'draft6', mod: :Draft06, image: 'draft6.jpg'},
]
drafts.each do |name: , mod: , image: |
  p [name, mod, image]
end
results in:
"3.1.0"
["draft4", "Draft04", "draft4.png"]
["draft6", "Draft06", "draft6.jpg"]
        
          
          Updated by Eregon (Benoit Daloze) over 3 years ago
          
          
        
        
      
      https://try.ruby-lang.org/ is Opal by default (clearer on https://try.ruby-lang.org/playground/), and that is not fully compatible with CRuby.
That explains what you saw, thanks for the reply.
=> https://github.com/ruby/TryRuby/issues/122
        
          
          Updated by Ethan (Ethan -) over 3 years ago
          
          
        
        
      
      thank you all for the feedback and suggestions, much appreciated.