Feature #15975
closedAdd Array#pluck
Description
Inspired by https://github.com/rails/rails/issues/20339
While developing web applications I've often wanted to quickly extract an array of values from an array of hashes.
With an array of objects, this is possible:
irb(main):001:0> require 'ostruct'
=> true
irb(main):002:0> [OpenStruct.new(name: "Lewis")].map(&:name)
=> ["Lewis"]
This PR adds Array#pluck allowing this:
irb(main):001:0> [ {name: "Lewis"} ].pluck(:name)
=> ["Lewis"]
without this PR:
irb(main):001:0> [ {name: "Lewis"} ].map { |item| item[:name] }
=> ["Lewis"]
Implemented here:
Updated by shevegen (Robert A. Heiler) over 5 years ago
Hmm. I don't doubt that this may possibly be useful, but the method name is
a bit ... weird. My first association with this name, oddly enough, is to
associate duck typing with it, and then to "pluck the duck" (yes, strange
association but I could not help it ...).
I do not have a better alternative suggestion for a name, though. It
reminds me a little bit of a more flexible variant of .dig(), though.
Updated by inopinatus (Joshua GOODALL) over 5 years ago
I think that's pretty limited. #pluck is a fairly crude method, fine for Rails but hardly befitting the Ruby standard library. I'd much rather use a higher-order function and get somewhere much more interesting.
By instead implementing Array#to_proc
(which doesn't currently exist) as something that applies to_proc to its own elements, before invoking them with passed-in arguments:
class Array
def to_proc
Proc.new do |head, *tail|
collect(&:to_proc).collect do |ep|
ep_head = ep[head]
tail.empty? ? ep_head : [ep_head] + tail.collect(&ep)
end
end
end
end
we can now do some nice things, including a pluck equivalent (and more besides) but using only existing syntax:
# data
people = [{name: "Park", age: 42}, {name: "Lee", age: 31}]
keys = people.flat_map(&:keys).uniq
# single item extraction
:name.then &people #=> ["Park", "Lee"] and equivalent to
people.to_proc[:name] #=> ["Park", "Lee"]
# multiple item extraction
keys.then &people #=> [["Park", 42], ["Lee", 31]] and equivalent to
people.to_proc[:name, :age] #=> [["Park", 42], ["Lee", 31]]
# multiple method invocation
:name.then(&people).map(&[:upcase, :length]) #=> [["PARK", 4], ["LEE", 3]]
# use with struct-like objects, and bonus inline lambda:
people.map(&OpenStruct.:new).map &[:name, :age, ->{ Digest::SHA2.hexdigest @1.name }]
Could work as Enumerable#to_proc
instead.
Updated by osyo (manga osyo) over 5 years ago
we can now do some very nice things just with existing syntax:
The sample code is invalid.
Is this?
class Array
def to_proc
Proc.new do |head, *tail|
collect(&:to_proc).collect do |ep|
ep_head = ep[head]
tail.empty? ? ep_head : [ep_head] + tail.collect(&ep)
end
end
end
end
# data
people = [{name: "Park", age: 42}, {name: "Lee", age: 31}]
# single item extraction
p :name.then &people #=> ["Park", "Lee"]
p people.to_proc[:name] #=> ["Park", "Lee"]
# multiple item extraction
p [:name, :age].then &people #=> [["Park", 42], ["Lee", 31]]
p people.to_proc[:name, :age] #=> [["Park", 42], ["Lee", 31]]
# multiple invocation
names = ["Park", "Lee"]
p names.map(&[:upcase, :length]) #=> [["PARK", 4], ["LEE", 3]]
Updated by inopinatus (Joshua GOODALL) over 5 years ago
My apologies, yes, there was a cut-and-paste error on show for a few minutes, and you were quick enough to see it. It's now the code I intended to paste.
Updated by knu (Akinori MUSHA) over 5 years ago
ActiveSupport has Enumerable#pluck, so I don't think we want to diverge from that by adding a method with the same name in Array.
Updated by matz (Yukihiro Matsumoto) over 5 years ago
I am not positive for Array#pluck
. ActiveSupport may add the method.
Matz.
Updated by connorshea (Connor Shea) about 4 years ago
I was going to suggest the same thing because I think it's a very useful shorthand! Here's an example I run into a lot when manipulating data in my Ruby scripts.
# Lets say I have an array of hashes representing video games (this is pretty
# common because I write a decent amount of scripts manipulating data in Ruby).
games = [
{
title: "Half-Life 2",
steam_id: 1
},
{
title: "Portal",
steam_id: 2
},
{
title: "Portal 2",
steam_id: 3
}
]
# If I want to get the Steam IDs for all those, for example to match this
# dataset with another dataset to find overlaps, I need to use a `map` like
# this:
games.map { |game| game[:steam_id] } #=> [1, 2, 3]
# That code above doesn't really spark joy, it's pretty lengthy for something
# that should be very simple.
# What I _want_ to do is something like this, but since these are just hash
# keys, I can't:
games.map(&:steam_id) #=> undefined method `steam_id'
# The best solution would be a `#pluck` method:
games.pluck(:steam_id) #=> [1, 2, 3]
# This sparks joy!
Please consider adding a #pluck
method on Enumerable
🙇♂️
Ideally it'd accept more than one argument to get multiple values at once, but that's not really a deal-breaker for me if we don't include it.
Maybe it could be called #pick
, #each_dig
, #map_keys
, or something else?
Updated by marcandre (Marc-Andre Lafortune) about 4 years ago
- Status changed from Open to Rejected
Matz has already stated that it's a no, so I will close this.
I'll add that the issue should not be about a shorthand to map
and calling []
or dig
, but how to write concisely {|game| game[:steam_id]}
. As @inopinatus (Joshua GOODALL) said, maybe a variant of to_proc
could make this more concise, but that is what _1
is for. This is short and concise:
games.map{ _1[:steam_id] }
Updated by phluid61 (Matthew Kerwin) about 4 years ago
Apologies for posting to a closed ticket, but here's a thought in case it helps someone propose something else in future: partial application in #to_proc
, e.g. games.map(&(:[], :steam_id))
I hate the syntax I just invented, but the idea of partial application to the right (i.e. applying args to a proc before applying the receiver) is interesting.