- Understand return values for enumerators.
- Use a truthy or falsey evaluation in a block.
- Use
#selectto select matching elements from a collection based on a block. - Use
#detectto find a matching element from a collection based on a block. - Use
#rejectto filter matching elements from a collection based on a block.
Every method in ruby must return a value. When we iterate or enumerate over a collection with #each, the return value is always the original collection. This is an example of a static return value, no matter what we do with #each, it will always return the same object that received the call to #each.
["Red", "Yellow", "Blue"].each do |color|
puts "There are #{color.length} letters in #{color}"
end #=> ["Red", "Yellow", "Blue"]Often we want to search for elements in a collection based on a condition. Imagine wanting to find all even numbers in a collection of numbers using #each.
matches = []
[1,2,3,4,5].each do |i|
matches << i if i.even? # add i to the matches array if it is even
end #=> [1,2,3,4,5]
matches #=> [2,4]Implementing a selection routine with a low-level enumerator like #each is costly in a few ways.
- We have to maintain state with the local array
matches. - Our block is complicated with conditional logic that can be implicit with a better enumerator.
- Our code lacks intention and clear semantics. If we mean,
#find_allor#select, why don't we just say that?
When you evoke #select on a collection, the return value will be a new array containing all the elements of the collection that cause the block passed to #select to return true. That means for each iteration, if the block evaluates to true, the element yielded to that iteration will kept in the return value array.
[1,2,3,4,5].select do |number|
number.even?
end #=> [2,4]In the first iteration of the block above, number will be assigned the value 1. Because 1.even? will return false, 1 will not be in the return array for this call to #select (same for 3 and 5). In the second iteration, number will be 2. Because 2.even? will return true, 2 will be in the return array (same for 4).
You can see the clarity and expressiveness of this syntax in the short block form below.
[1,2,3,4,5].select{|i| i.odd?} #=> [1,3,5]
[1,2,3].select{|i| i.is_a?(String)} #=> []Notice that if no element makes the block evaluate to true, an empty array is returned.
Whereas #select will return all elements from the original collection that cause the block to evaluate to true, #detect will only return the first element that makes the block true.
[1,2,3].detect{|i| i.odd?} #=> 1As you can see, even though both 1 and 3 would cause the block to evaluate to true, because 1 is first in the array, it alone is returned.
[1,2,3,4].detect{|i| i.even?} #=> 2
[1,2,3,4].detect{|i| i.is_a?(String)} #=> nilNotice also that #detect will always return a single object where #select will always return an array.
#reject will return an array with the elements that make the block true removed.
[1,2].reject{|i| i.even?} #=> [1]#select, #detect, and #reject are part of a family of search and filter type enumerators whose purpose is to help you refine a collection to only matching elements. They are way easier to manage then using lower-level methods like #each and create meaningful return values based on expressions in a block.