Here’s another article in the Enumerating Enumerable series, in which I attempt to improve upon RubyDoc.org’s documentation for the Enumerable
module, which I find rather lacking. If you’ve missed the previous articles in the series, I’ve listed them below:
This installment covers a method that goes by two names: detect
or find
. I personally prefer find
, as it’s shorter and the term I tend to use for its function.
Enumerable#detect/Enumerable#find Quick Summary
In the simplest possible terms | What’s the first item in the collection that meets the given criteria? |
---|---|
Ruby version | 1.8 and 1.9 |
Expects |
|
Returns | The first item in the collection that matches the criteria, if one exists. If no such item exists in the collection, detect /find returns:
|
RubyDoc.org’s entry | Enumerable#detect / Enumerable#find |
Enumerable#detect/Enumerable#find and Arrays
When used on an array without an argument, detect
/find
passes each item from the collection to the block and…
- If the current item causes the block to return a value that doesn’t evaluate to
false
,detect
/find
stops going through collection and returns the item. - If no item in the collection causes the block to return a value that doesn’t evaluate to
false
,detect
/find
returnsnil
.
In the examples that follow, I’ll be using the find
method. detect
does exactly the same thing; it’s just that I prefer find
.
# Time to establish my "old fart" credentials classic_rock_bands = ["AC/DC", "Black Sabbath", "Queen", \ "Ted Nugent and the Amboy Dukes", "Scorpions", "Van Halen"] => ["AC/DC", "Black Sabbath", "Queen", "Ted Nugent and the Amboy Dukes", "Scorpions", "Van Halen"] # Of the bands in the array, which is the first one # whose name is longer than 8 characters? classic_rock_bands.find {|band| band.length > 8} => "Black Sabbath" # Which is the first one whose name is exactly # 5 characters long? classic_rock_bands.find {|band| band.length == 5} => "AC/DC" # The order of items in the array can affect "find"'s result. # Let's put the array into reverse sorted order: classic_rock_bands.sort!.reverse! => ["Van Halen", "Ted Nugent and the Amboy Dukes", "Scorpions", \ "Queen", "Black Sabbath", "AC/DC"] # Again: which is the first band whose name # is longer than 8 characters? => "Van Halen" # Again: which is the first band whose name # is exactly 5 characters? => "Queen" # If no band in the array meets the criteria, # "find" returns nil. # There are no bands in the list with names shorter # than 5 characters... classic_rock_bands.find {|band| band.length < 5} => nil
Using the optional argument is a topic big enough to merit its own section, which appears later in this article.
Enumerable#detect/Enumerable#find and Hashes
When used on a hash and a block is provided, detect
/find
passes each key/value pair in the hash to the block, which you can “catch” as either:
- A two-element array, with the key as element 0 and its corresponding value as element 1, or
- Two separate items, with the key as the first item and its corresponding value as the second item.
When used on a hash without an argument, detect
/find
passes each item from the collection to the block and…
- If the current item causes the block to return a value that doesn’t evaluate to
false
,detect
/find
stops going through collection and returns the item. - If no item in the collection causes the block to return a value that doesn’t evaluate to
false
,detect
/find
returnsnil
.
years_founded = {"AC/DC" => 1973, \ "Black Sabbath" => 1968, \ "Queen" => 1970, \ "Ted Nugent and the Amboy Dukes" => 1967, \ "Scorpions" => 1965, \ "Van Halen" => 1972} # Ruby 1.8 re-orders hashes in some mysterious way that is almost never # the way you entered it. Here's what I got in Ruby 1.8: => {"Queen"=>1970, "AC/DC"=>1973, "Black Sabbath"=>1968, "Scorpions"=>1965, "Ted Nugent and the Amboy Dukes"=>1967, "Van Halen"=>1972} # Ruby 1.9 preserves hash order so that hashes keep the order in which # you defined them. Here's what I got in Ruby 1.9: => {"AC/DC"=>1973, "Black Sabbath"=>1968, "Queen"=>1970, "Ted Nugent and the Amboy Dukes"=>1967, "Scorpions"=>1965, "Van Halen"=>1972} # Which band is the first in the hash to be founded # in 1970 or later? years_founded.find {|band| band[1] >= 1970} # In Ruby 1.8, the result is: => ["Queen", 1970] # In Ruby 1.9, the result is: => ["AC/DC", 1973] # Here's another way of phrasing it: years_founded.find {|band, year_founded| year_founded >= 1970}
Using Enumerable#detect/Enumerable#find with the Optional Argument
detect
/find
‘s optional argument lets you specify a proc or lambda whose return value will be the result in cases where no object in the collection matches the criteria.
(Unfortunately, a complete discussion of procs and lambdas is beyond the scope of this article. I highly recommend looking at Eli Bendersky’s very informative article, Understanding Ruby blocks, Procs and methods.)
I think that the optional argument is best explained through examples…
# Once again, the array of classic rock bands classic_rock_bands = ["AC/DC", "Black Sabbath", "Queen", \ "Ted Nugent and the Amboy Dukes", "Scorpions", "Van Halen"] # Let's define a proc that will simply returns the band name # "ABBA", who are most definitely not considered to be # a classic rock band. default_band = Proc.new {"ABBA"} # Procs are objects, so using a proc's name alone isn't sufficient to invoke its code -- doing so will simply return the proc object. default_band => #<Proc:0x00553f34@(irb):31> # (The actual value will be different for you, but you get the idea.) # To call a proc, you have to use its "call" method: default_band.call => "ABBA" # What we want to do is use "default_band" as a proc that # provides a default value in the event that detect/find doesn't # find a value that matches the critera. # detect/find calls the "call" method of the object you provide # as the argument if no item in the collection matches the # criteria in the block. # There *is* a band in the array that comes after # "Led Zeppelin" alphabetically: Queen. classic_rock_bands.find(default_band) {|band| band > "Led Zeppelin"} => "Queen" # The last band in the array, alphabetically speaking, # is Van Halen. So if we ask detect/find to find a band that # comes after Van Halen, it won't find one. # Without the argument, detect/find returns nil, # but *with* the argument, it will invoke the "call" # method of the object you provide it. classic_rock_bands.find(default_band) {|band| band > "Van Halen"} => "ABBA" # Let's try something a little fancier. This time, we'll use a lambda. # The differences between procs and lambdas are very fine -- I suggest # you check Eli Bendersky's article for those differences. # Let's create a lambda that when called, # returns the name of a randomly-selected pop singer. random_band = lambda do fallback_bands = ["Britney Spears", "Christina Aguilera", "Ashlee Simpson"] fallback_bands[rand(fallback_bands.size)] end # Let's give it a try... classic_rock_bands.find(random_band) {|band| band > "Van Halen"} => "Britney Spears" classic_rock_bands.find(random_band) {|band| band > "Van Halen"} => "Ashlee Simpson" classic_rock_bands.find(random_band) {|band| band > "Van Halen"} => "Christina Aguilera" classic_rock_bands.find(random_band) {|band| band > "Led Zeppelin"} => "Queen"
To see a “real” application of detect
/find's
optional argument, see this Ruby Quiz problem.