Feature #13683
Add strict Enumerable#single
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
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
toEnumerable#first
orEnumerable#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) over 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 exactly1
#one
- raise an error if the result's#size
is greater than1
. Return the result of#first
otherwise (so an empty result returnsnil
).
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) over 1 year ago
nobu (Nobuyoshi Nakada) wrote:
How about
Enumerable#just(num=1)
orEnumerable#only(num=1)
?
Or maybe a slightly more verbose Enumerable#first_and_only(num = 1)
?
Updated by lugray (Lisa Ugray) 9 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) 2 months 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) 2 months 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
Updated by matz (Yukihiro Matsumoto) about 2 months ago
I don't like only
either since these names do not describe the behavior.
Matz.
Updated by kinoppyd (Yasuhiro Kinoshita) about 2 months ago
[1, 2].mono [1, 2].solo [1, 2].alone
Updated by Hanmac (Hans Mackowiak) about 2 months ago
Dan0042 (Daniel DeLorme) wrote:
+1
I actually have this as
single
in my own code, butonly
sounds fine also. I'd want a non-raising version (perhaps via araise
keyword arg?), as my usage tends to be like this:if match = filenames.select{ |f| f.start_with?(prefix) }.single redirect_to match end
instead of #select
, shouldn't you use #find
so it doesn't need to check the others when it already found a match?
Updated by Dan0042 (Daniel DeLorme) about 2 months ago
instead of
#select
, shouldn't you use#find
so it doesn't need to check the others when it already found a match?
No, because it should return nil when there's more than one match.
Updated by kaikuchn (Kai Kuchenbecker) about 1 month ago
I like one
a lot. Especially since there's already one?
.