Project

General

Profile

Feature #13683

Add strict Enumerable#single

Added by dnagir (Dmytrii Nagirniak) over 2 years ago. Updated 7 days ago.

Status:
Feedback
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:81794]

Description

Summary

This is inspired by other languages and frameworks, such as LINQ's Single (pardon MSDN reference), which has very big distinction between first and single element of a
collection.

  • first normally returns the top element, and the developer assumes there could be many;
  • single returns one and only one element, and it is an error if there are none or more than one.

We, in Ruby world, very often write fetch_by('something').first
assuming there's only one element that can be returned there.

But in majority of the cases, we really want a single element.

The problems with using first in this case:

  • developer needs to explicitly double check the result isn't nil
  • in case of corrupted data (more than one item returned), it will never be noticed

Enumerable#single addresses those problems in a very strong and
specific way that may save the world by simply switching from first to
single.

Other information

  • we may come with a better internal implementation (than self.map)
  • better name could be used, maybe only is better, or a bang version?
  • re-consider the "block" implementation in favour of a separate method (single!, single_or { 'default' })

The original implementation is on the ActiveSupport https://github.com/rails/rails/pull/26206
But it was suggested to discuss the possibility of adding it to Ruby which would be amazing.

History

#1

Updated by dnagir (Dmytrii Nagirniak) over 2 years ago

  • Tracker changed from Bug to Feature

Updated by Eregon (Benoit Daloze) over 2 years ago

+1, I have found this useful a few times as well.
Usually, I just define my own on Array, but it makes sense as well for Enumerable.

Updated by shevegen (Robert A. Heiler) over 2 years ago

I am not against or in favour of it but just a question.

What would the results be for the following code? In ruby (I find
it easier to read ruby code rather than the description actually):

[].single
[1].single
[1,2].single
[1,2,3].single

{}.single
{cat: 'Tom'}.single
{cat: 'Tom', mouse: 'Jerry'}.single

(And any other Enumerable objects I may have forgotten here.)

Updated by mame (Yusuke Endoh) over 2 years ago

+1. I always feel uncomfortable whenever using first for this purpose.

Updated by shan (Shannon Skipper) over 2 years ago

shevegen (Robert A. Heiler) wrote:

What would the results be for the following code? In ruby (I find
it easier to read ruby code rather than the description actually):

[].single
[1].single
[1,2].single
[1,2,3].single

{}.single
{cat: 'Tom'}.single
{cat: 'Tom', mouse: 'Jerry'}.single

(And any other Enumerable objects I may have forgotten here.)

I wrote a quick Ruby implementation before realizing there was a link to a Rails PR. Here are the results of your examples (and one added):

module Enumerable
  def single
    if one?
      first
    else
      if block_given?
        yield
      else
        raise "wrong collection size (actual #{size || count}, expected 1)"
      end
    end
  end
end

[].single
#!> RuntimeError: wrong collection size (actual 0, expected 1)

[1].single
#=> 1

[1,2].single
#!> RuntimeError: wrong collection size (actual 2, expected 1)

[1,2,3].single
#!> RuntimeError: wrong collection size (actual 3, expected 1)

{}.single
#!> RuntimeError: wrong collection size (actual 0, expected 1)

{cat: 'Tom'}.single
#=> [:cat, "Tom"]

{cat: 'Tom', mouse: 'Jerry'}.single
#!> RuntimeError: wrong collection size (actual 2, expected 1)

[].single { 42 }
#=> 42

Edit: Caveat, my implementation doesn't handle an Infinite unsized enumerator, unlike the Rails PR which does.

Updated by nobu (Nobuyoshi Nakada) over 2 years ago

  • Description updated (diff)

Enumerable#first returns not only the first element, the elements at the beginning up to the number given by an optional argument.

How about an optional boolean argument exact to Enumerable#first or Enumerable#take?

Updated by dnagir (Dmytrii Nagirniak) over 2 years ago

shevegen (Robert A. Heiler) wrote:

What would the results be for the following code?

I would expect the following:

[].single # => error
[1].single # =>1
[1,2].single # => error
[1,2,3].single # => error

{}.single # => error
{cat: 'Tom'}.single # same as .first => [:cat, 'Tom']
{cat: 'Tom', mouse: 'Jerry'}.single # error

Updated by dnagir (Dmytrii Nagirniak) over 2 years ago

nobu (Nobuyoshi Nakada) wrote:

Enumerable#first returns not only the first element, the elements at the beginning up to the number given by an optional argument.

How about an optional boolean argument exact to Enumerable#first or Enumerable#take?

The purpose of the single suggested is to return one and only one element.
So it doesn't seem right to mix it up with first as it'll only add confusion, especially when used with a block.

On the other hand, I feel like a separate method that does one small thing well would be a much better API.

Updated by backus (John Backus) about 2 years ago

+1 to this proposal!! I have a Util.one(...) method in a half dozen or more projects. IMO #one is a nicer name than #single.

ROM exposes an interface I like when reading results from the db:

  • #one! - raise an error unless the result's #size is exactly 1
  • #one - raise an error if the result's #size is greater than 1. Return the result of #first otherwise (so an empty result returns nil).

I don't think the implementation should use the #one? predicate though. It would be confusing if [nil, true, false].single gave you nil instead of raising an error.

Updated by matz (Yukihiro Matsumoto) about 2 years ago

  • Status changed from Open to Feedback

Hmm, I don't like the name single. Besides that, I think it may be useful for database access, but I don't see the use-case of this method for generic Enumerable.

Matz.

Updated by IotaSpencer (Ken Spencer) over 1 year ago

matz (Yukihiro Matsumoto) wrote:

Hmm, I don't like the name single. Besides that, I think it may be useful for database access, but I don't see the use-case of this method for generic Enumerable.

Matz.

I think of single as a method towards mutual exclusivity.
If an Array or Enumerable from another expression should only have a single element,
then this gives the process a much faster setup and possible rescue, as I currently have
one of my projects checking for the existence of 3 headers, X-GitHub-Event, X-GitLab-Event,
and X-Gogs-Event, and I found the easiest way was to use one from Enumerable, but I wanted it
to error out so that I could catch it with the rest of my raised exceptions from other errors that
arise in the handling of the request.

How about these for suggestions.

one_or_raise
one_or_nothing

Part of my code for context.

      events = {'github' => github, 'gitlab' => gitlab, 'gogs' => gogs
      }
      events_m_e = events.values.one?
      case events_m_e
        when true
          event = 'push'
          service = events.select { |key, value| value }.keys.first
        when false
          halt 400, {'Content-Type' => 'application/json'}, {message: 'events are mutually exclusive', status: 'failure'
          }.to_json

        else halt 400, {'Content-Type' => 'application/json'}, {'status': 'failure', 'message': 'something weird happened'
        }
      end

Updated by nobu (Nobuyoshi Nakada) over 1 year ago

How about Enumerable#just(num=1) or Enumerable#only(num=1)?

Updated by shan (Shannon Skipper) about 1 year ago

nobu (Nobuyoshi Nakada) wrote:

How about Enumerable#just(num=1) or Enumerable#only(num=1)?

Or maybe a slightly more verbose Enumerable#first_and_only(num = 1)?

Updated by lugray (Lisa Ugray) 6 months ago

I was pointed here after sharing the following code with my team mates. I really like the idea, and find I often reach for it. I second the name only.

module Enumerable
  def only
    only!
  rescue IndexError
    nil
  end

  def only!
    raise(IndexError, "Count (#{count}) is not 1") if count != 1
    first
  end
end

Updated by jonathanhefner (Jonathan Hefner) 7 days ago

matz (Yukihiro Matsumoto) wrote:

Hmm, I don't like the name single. Besides that, I think it may be useful for database access, but I don't see the use-case of this method for generic Enumerable.

I use (monkey-patched) Enumerable#single in Ruby scripts which must fail fast when they encounter ambiguity. For example Nokogiri::HTML(html).css(selector).single to ensure an unambiguous matching HTML element. Or Dir.glob(pattern).single to ensure an unambiguous matching file.

Also, I agree that only would be a better name. And it would read more naturally if accepting an n argument like Enumerable#first does.

Updated by Dan0042 (Daniel DeLorme) 7 days ago

+1

I actually have this as single in my own code, but only sounds fine also. I'd want a non-raising version (perhaps via a raise keyword arg?), as my usage tends to be like this:

if match = filenames.select{ |f| f.start_with?(prefix) }.single
  redirect_to match
end

Also available in: Atom PDF