Feature #13683
closedAdd 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.
Updated by dnagir (Dmytrii Nagirniak) over 7 years ago
- Tracker changed from Bug to Feature
Updated by Eregon (Benoit Daloze) over 7 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 7 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 7 years ago
+1. I always feel uncomfortable whenever using first
for this purpose.
Updated by shan (Shannon Skipper) over 7 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 7 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 7 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 7 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) about 7 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 7 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 6 years 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 6 years ago
How about Enumerable#just(num=1)
or Enumerable#only(num=1)
?
Updated by shan (Shannon Skipper) about 6 years 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) over 5 years 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) about 5 years 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) about 5 years 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) almost 5 years ago
I don't like only
either since these names do not describe the behavior.
Matz.
Updated by kinoppyd (Yasuhiro Kinoshita) almost 5 years ago
[1, 2].mono
[1, 2].solo
[1, 2].alone
Updated by Hanmac (Hans Mackowiak) almost 5 years 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) almost 5 years 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) almost 5 years ago
I like one
a lot. Especially since there's already one?
.
Updated by fatkodima (Dima Fatko) about 4 years ago
I have opened a PR - https://github.com/ruby/ruby/pull/3470
# Returns one and only one item. Raises an error if there are none or more than one.
[99].one #=> 99
[].one #=> RuntimeError: collection is empty
[99, 100].one #=> RuntimeError: collection contains more than one item
# If collection is empty and no block was given, returns default value:
[].one(99) #=> 99
# If collection is empty and a block was given, returns the block's return value:
[].one { 99 } #=> 99
Updated by marcandre (Marc-Andre Lafortune) about 4 years ago
If we introduce one
, it would be nice to support regexp; maybe use ===
for matching when given an argument?
%w[hello world].one(/ll/) # => 'hello'
Updated by sawa (Tsuyoshi Sawada) about 4 years ago
Having Enumerable#find
take an optional keyword argument, say exception:
, would make more sense, be useful, and have more generality.
[1].find(exception: true){true} # => 1
[1, 2, 3].find(exception: true){true} # >> Error
[].find(exception: true){true} # >> Error
%w[hello world].find(exception: true){/ll/ === _1} # => "hello"
%w[hello world].find(exception: true){/l/ === _1} # => Error
%w[hello world].find(exception: true){/x/ === _1} # => Error
Updated by Dan0042 (Daniel DeLorme) about 4 years ago
If collection is empty and a block was given, returns the block's return value:
I really think the block form should be like find/select
[1,2,3].one{ _1.even? } #=> 2
[1,2,3,4].one{ _1.even? } #=> error
[1,2,3,4].one(nil){ _1.even? } #=> nil
Having
Enumerable#find
take an optional keyword argument, sayexception:
, would make more sense, be useful, and have more generality.
I don't think so; find only returns the first element found. An argument that makes it continue searching and return an exception if it finds a second match... that alters the fundamental behavior too much imho.
Updated by sawa (Tsuyoshi Sawada) about 4 years ago
Dan0042 (Daniel DeLorme) wrote in #note-25:
I really think the block form should be like find/select
[1,2,3].one{ _1.even? } #=> 2 [1,2,3,4].one{ _1.even? } #=> error [1,2,3,4].one(nil){ _1.even? } #=> nil
...
continue searching [...] that alters the fundamental behavior too much imho.
I think you are right.
But the word "one", which has the same etymology as the indefinite article "an", is not appropriate here. Its meaning is to arbitrarily pick out a single entity. This does match the concept discussed here.
I think the English word that best matches the concept is the definite article. This word presupposes that there is exactly one corresponding entity in the context, and picks that entity. If the presupposition is not satisfied, then the entire expression would be uninterpretable in the case of natural languages, which amounts to raising an exception in the case of programming languages.
[1, 2, 3].the(&:even?) # => 2
[1, 2, 3, 4].the(&:even?) # >> error
[1, 3].the(&:even?) # >> error
Using the usual block parameter, [1,2,3].the{|x| x.even?}
could be read as "the x
(out of [1, 2, 3]
) such that x
is even." If such x
does not uniquely exist, this expression is uninterpretable.
Updated by Dan0042 (Daniel DeLorme) about 4 years ago
Hmmm, just now I realized there's a simple idiom that's roughly equivalent to one
/single
[1,2].inject{break} #=> nil
[1,2].inject{raise} #=> error
[1].inject{break} #=> 1
[1].inject{raise} #=> 1
[].inject{break} #=> nil
[].inject{raise} #=> nil (instead of error)
Updated by Eregon (Benoit Daloze) about 4 years ago
matz (Yukihiro Matsumoto) wrote in #note-10:
Hmm, I don't like the name
single
.
matz (Yukihiro Matsumoto) wrote in #note-17:
I don't like
only
either since these names do not describe the behavior.
Could you explain why?
I think single
as in "return a single element or error out" is the best name here, and the most intuitive.
If I want a random element, then #sample already exists, so I don't think there would be any confusion to what Array#single
or Enumerable#single
would do.
array.only
reads weirdly to me. array.single
is so much nicer, no?
Updated by shan (Shannon Skipper) about 4 years ago
How about #sole since it means one and only and is concise?
[].sole
#!> SoleError: empty Array when single value expected (contains 0, expected 1)
Set.new.sole
#!> SoleError: empty Set when single value expected (contains 0, expected 1)
[41, 42, 43].sole
#!> SoleError: multiple values in Array when just one expected (contains 3, expected 1)
[42].sole
#=> 42
Or #one_and_only, but it's more wordy.
Updated by nobu (Nobuyoshi Nakada) about 4 years ago
Dan0042 (Daniel DeLorme) wrote in #note-25:
If collection is empty and a block was given, returns the block's return value:
I really think the block form should be like find/select
[1,2,3].one{ _1.even? } #=> 2 [1,2,3,4].one{ _1.even? } #=> error [1,2,3,4].one(nil){ _1.even? } #=> nil
It looks close to Enumerable#one?
which counts truthy values only, but has a different semantics.
Updated by mame (Yusuke Endoh) about 4 years ago
A practical use case: When scraping a HTML document or something, sometimes we assume that an array length is 1.
nodes = html_node.children.select {|node| node.name == "table" }
raise if nodes.size == 1
the_node_i_want = nodes.first
The raise
is needed for the case where my assumption is wrong.
This proposal makes it a bit helpful:
the_node_i_want = html_node.children.select {|node| node.name == "table" }.sole
Updated by nobu (Nobuyoshi Nakada) about 4 years ago
What about pattern matching?
case []; in [a]; p a; end #=> NoMatchingPatternError ([])
case [1]; in [a]; p a; end #=> 1
case [1,2]; in [a]; p a; end #=> NoMatchingPatternError ([1, 2])
Updated by shan (Shannon Skipper) about 4 years ago
For Arrays, pattern matching does seem like a fair solution. Wouldn't that not work for other Enumerable collections?
Set.new([42]) in [one] #!> NoMatchingPatternError (#<Set: {42}>)
It seems an Enumerable method would be more slick and nice for method chaining.
Set.new([42]).sole #=> 42
Set.new([42]).sole.digits #=> [2, 4]
Updated by mame (Yusuke Endoh) about 3 years ago
- Related to Feature #18135: Introduce Enumerable#detect_only added
Updated by meisel (Michael Eisel) about 3 years ago
+1 for this, I've needed it in many cases, generally where I have to select an element from a set and there's no stable/documented way of doing this. So, I find some criteria that seem to work, but would like to protect against my criteria being wrong or invalidated by future changes. In the past, I've needed it for sets like sibling directories and sibling XML nodes.
As for the name, I think #take_only
would be another reasonable option