Project

General

Profile

Actions

Misc #18609

closed

keyword decomposition in enumerable (question/guidance)

Added by Ethan (Ethan -) almost 3 years ago. Updated almost 3 years ago.

Status:
Closed
Assignee:
-
[ruby-core:107769]

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) almost 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) almost 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) almost 3 years ago

interesting, it seems to be changed in between "3.1.0" and "3.1.1"

Updated by baweaver (Brandon Weaver) almost 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) almost 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) almost 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) almost 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) almost 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 -) almost 3 years ago

thank you all for the feedback and suggestions, much appreciated.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0