Project

General

Profile

Actions

Feature #15975

closed

Add Array#pluck

Added by lewispb (Lewis Buckley) over 2 years ago. Updated 10 months ago.

Status:
Rejected
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:93486]

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:

https://github.com/ruby/ruby/pull/2263

Updated by shevegen (Robert A. Heiler) over 2 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 2 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 2 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]]

https://wandbox.org/permlink/4oVOzULhwKsu4gB5

Updated by inopinatus (Joshua GOODALL) over 2 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) about 2 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) about 2 years ago

I am not positive for Array#pluck. ActiveSupport may add the method.

Matz.

Updated by connorshea (Connor Shea) 11 months 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) 11 months 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) 10 months 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.

Actions

Also available in: Atom PDF