Project

General

Profile

Actions

Feature #18369

open

users.detect(:name, "Dorian") as shorthand for users.detect { |user| user.name == "Dorian" }

Added by dorianmariefr (Dorian Marié) 7 months ago. Updated 7 months ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:106422]

Description

Hi,

I was thinking I often do things like collection.detect { |item| item.attribute == value } and a shorthand like collection.detect(:attribute, value) would be quite useful

What do you think?

And I know there is collection.detect { _1.attribute == value } but I try not to use _1 and this syntax would be shorter and simpler

Could also apply to other methods like all? (collection.all?(:attribute, value)), and basically any Enumerable method https://rubydoc.info/stdlib/core/Enumerable

Actions #1

Updated by dorianmariefr (Dorian Marié) 7 months ago

  • Backport deleted (2.6: UNKNOWN, 2.7: UNKNOWN, 3.0: UNKNOWN)
  • Tracker changed from Bug to Feature

Updated by dorianmariefr (Dorian Marié) 7 months ago

Could also be users.detect(&:name, "Dorian")

Updated by Dan0042 (Daniel DeLorme) 7 months ago

So you try not to use _1 ... just out of curiosity, would you use this?
collection.detect{ .attribute == value }

Updated by dorianmariefr (Dorian Marié) 7 months ago

Dan0042 (Daniel DeLorme) wrote in #note-3:

So you try not to use _1 ... just out of curiosity, would you use this?
collection.detect{ .attribute == value }

I don't think so, still not explicit what is going on, and there is the overhead of new syntax

Updated by sawa (Tsuyoshi Sawada) 7 months ago

I think that is too specific to be a part of Ruby core. I don't think this feature would be accepted.

I think you can define a Proc constructor method for yourself like the following:

def attreql k, v
   Proc.new{_1.send(k) == v}
end

Then, you can do:

class A
  attr_reader :foo, :bar

  def initialize foo, bar
    @foo, @bar = foo, bar
  end
end

collection = [A.new(1, 2), A.new(3, 4), A.new(5, 6)]

collection.detect(&attreql(:foo, 3)) # => #<A:0x00007fb751064630 @foo=3, @bar=4>
collection.all?(&attreql(:bar, 7)) # => false

The strength of doing it like this compared to your proposal is that it is more flexible. You can do:

def attrlt k, v
   Proc.new{_1.send(k) < v}
end

collection.detect(&attrlt(:foo, 3)) # => #<A:0x00007fd3ab8a4680 @foo=1, @bar=2>
collection.all?(&attrlt(:bar, 7)) # => true

Updated by dorianmariefr (Dorian Marié) 7 months ago

Maybe the feature would be to be possible to have arguments after a block, e.g.

def detect(&block, value)
  User.all.detect { |user| block.call(user) == value }
end

detect(&:first_name, "Dorian")

Updated by nobu (Nobuyoshi Nakada) 7 months ago

Since Enumerable#detect or Enumerable#find has the argument for the different purpose, I think that the extension in this way is not acceptable and should be a separate method.

The "arguments after a block" is one of rejected ideas before the numbered parameters.

Updated by cvss (Kirill Vechera) 7 months ago

It's a good occasion to use the composition of Proc/Method objects:

collection.detect(&:first_name.to_proc>>"Dorian".method(:==))

If we had a shorthand operator for Object#method (#12125), it would look nicer:

collection.detect(&:first_name.to_proc>>"Dorian".:==)

And if we make a shorthand Symbol#>> for the composition of a Symbol and a Proc, it would look even wonderful:

class Symbol
  def >> b
    to_proc >> b
  end
end

collection.detect(&:first_name>>"Dorian".:==)

When you are frequently using such constructions you can read it easily, but it is definitely more confusing comparing to the old plain variant:

collection.detect{_1.first_name == "Dorian"}

Updated by sawa (Tsuyoshi Sawada) 7 months ago

cvss (Kirill Vechera) wrote in #note-8:

It's a good occasion to use the composition of Proc/Method objects:

collection.detect(&:first_name.to_proc>>"Dorian".method(:==))

Your trick forces the use of Yoda conditions, which may be tricky and cryptic.

Updated by baweaver (Brandon Weaver) 7 months ago

Pattern Matching may make a very interesting tie-in here for a short-hand:

# Struct provides built-in pattern matching abilities
Person = Struct.new(:first_name, :last_name, :age)

jim = Person.new("Jim", "Smith", 30)
jill = Person.new("Jill", "Smith", 20)
sue = Person.new("Sue", "Smith", 40)

people = [jim, jill, sue]

# Currently works
people.select { _1 in { first_name: /^J/, age: 18.. } }

# Potential 1: bare keywords
people.select { _1 in first_name: /^J/, age: 18.. }

# Potential 2: `in` shorthand
people.select(&in first_name: /^J/, age: 18..)

Generally I think 1 is doable, 2 is stretching, though it would be nice to have syntax that allows to shorten one-line matchers for predicates where they would be commonly used.

Updated by zverok (Victor Shepelev) 7 months ago

it would be nice to have syntax that allows to shorten one-line matchers for predicates where they would be commonly used

TBH, since pattern matching inception I hope for some way of putting patterns into values—to store them in constants, and, in that case, simple grep will do (if that value would respond to #=== which it should!):

MY_PATTERN = _pm_(first_name: /^J/, age: 18..)

# ...and then 
if value in MY_PATTERN ...

# ...and, consequently, 
people.grep(_pm_(first_name: /^J/, age: 18..))

(I am marking the dreamed-of PM constructor as ugly _pm_ here to underline it is not a ready proposal, but "something should be here")

Updated by Dan0042 (Daniel DeLorme) 7 months ago

zverok (Victor Shepelev) wrote in #note-11:

TBH, since pattern matching inception I hope for some way of putting patterns into values

Close enough?

MY_PATTERN = proc{ _1 in {name: /^B/, age: 18..} }
people = [{:name=>"Jim", :age=>18}, {:name=>"Bob", :age=>40}]
people[0] in MY_PATTERN #=> false
people[1] in MY_PATTERN #=> true
people.grep(MY_PATTERN) #=> [{:name=>"Bob", :age=>40}]

Updated by zverok (Victor Shepelev) 7 months ago

Close enough?

Obviously :) But still a "hack". E.g. it is not a "value representing the pattern", this way we can talk ourselves into "we didn't need PM at all, we always could

MY_PATTERN = -> { _1[:name] =~ /^B/ && _1[:age] > 18 }

# ...and 
case foo
when -> { _1[:name] =~ /^B/ && _1[:age] > 18 }

:shrug:

Actions

Also available in: Atom PDF