As we mentioned at the end of last chapter, Ruby blocks are really useful for doing things to collections of objects. Imagine that you had a list of dollar amounts, and you wanted to convert it into a list of euro amounts. You could do that with theArray#map
method in Ruby:
dollar_amounts = [2347, 5834, 89, 89533]
EXCHANGE_RATE = 0.865448692
euro_amounts = dollar_amounts.map do |amount|
(amount * EXCHANGE_RATE).round(2)
end
=> [2031.21, 5049.03, 77.02, 77486.22]
The block that we pass in our call tomap
is called one time for each element in the array. Each time the block is called, the value of the current array element is passed in as the block parameter. The results of running each block are stored as new elements in a new array. After this code is run, we have two separate arrays:
dollar_amounts = [2347, 5834, 89, 89533]
euro_amounts = [2031.21, 5049.03, 77.02, 77486.22]
Hashes
Arrays aren't the only class of object that you can iterate over.
Hashes are like arrays, but different. They're a collection of objects that aren't really in any particular order, but that each has a name you can use to find it in the collection. If an array is like a group of people waiting in a line to be served at a takeout counter, a hash is like a group waiting to be seated at a restaurant, where each person gives their name and then just waits wherever outside. When the host is ready to seat them, they go outside and call them by name.
my_hash = {
alessi: 8,
hanh: 2,
donnelly: 2,
shaw: 5,
stein: 2,
ngabe: 4
}
my_hash[:alessi]
=> 8
my_hash[:ngabe]
=> 4
You can iterate over hashes as well. Their order isn't guaranteed like that of Arrays. It's like taking each person out of the crowd, one-at-a-time, until you've gotten to everyone. Here's how we can map a hash to create an array of arrays:
my_hash.map do |last_name, number_in_party|
[
last_name,
number_in_party >= 4 ? :big_table : :small_table
]
end
=> [[:alessi, :big_table], [:hanh, :small_table], [:donnelly, :small_table], [:shaw, :big_table], [:stein, :small_table], [:ngabe, :big_table]]
In a previous chapter, we also looked at how different classes of numbers in Ruby shared behaviors through the Numeric class. In the same way, collections of data likeArray
andHash
share behaviors through the Enumerable module.
Let's take a look at the documentation for Enumerable, for the 2.4.1 version of Ruby:
https://ruby-doc.org/core-2.4.1/Enumerable.html
These methods provide great examples of the usefulness of blocks. They're also some of the most-used tools for the day-to-day work of a Ruby developer.
This is also a good chance to get familiar with the Ruby docs, which are the standard reference for how the Ruby language works.
On the left side of this page you see a list of instance methods that come with including the Enumerable module. You can click on any of those methods to visit its documentation in the page.
all?
Like many enumerable methods, the block passed in to *all? is expected to be a kind of custom test for some condition that you devise. Specifically, *all? tells you if every element in the collection passes your test:
[1,2,3,4,5].all? { |i| i < 100 }
=> true
[1,2,3,4,5].all? { |i| i < 5 }
=> false
Truthiness
Remember, just as with a method, the result of evaluating the last line in your block (in this case, the only line), becomes the value that is returned by the block. Methods likeall?
will try to interpret the value you return as atrue
orfalse
. In the example above, the<
method does return a boolean value, so that works perfectly for our test. But you can also return any kind of value you like, and Ruby will make a guess as to whether it should be considered truthy or falsy. Objects of most types are considered truthy. This goes for all numbers and strings, even0
and""
. The objectnil
– and of course the objectfalse
itself – are considered falsy.
any?
any?
works much likeall?
, except that it tells you if any element passes your test:
[1,2,3,4,5].any? { |i| i == 6 }
=> false
[1,2,3,4,5].any? { |i| i == 5 }
=> true
count
*count is different depending on whether or not you pass a block. If you don't pass a block, it just tells you how many elements are in the collection:
[1,2,3,4,5].count
=> 5
If you do pass a block, it treats it like a test, and tells how many elements pass:
[1,2,3,4,5].count { |i| i.even? }
=> 2
find
, akadetect
like any?, but returns the first element that passes your test
[1,2,3,4,5].find { |i| i.even? }
=> 2
[1,2,3,4,5].detect { |i| i.even? }
=> 2
drop
returns a new array without the first n elements
[1,2,3,4,5].drop(3)
=> [4,5]
drop_while
like drop, but drops everything up until but not including the first element that fails your test
[1,2,3,4,5].drop_while { |i| i == 3 }
=> [3,4,5]
find_all
returns a new array with all the elements that pass your test
[1,2,3,4,5].find_all { |i| i.odd? }
=> [1,3,5]
select
does the same thing
reject
like find_all and select, except it returns a new array with all the elements that _don't pass your test:
[1,2,3,4,5].reject { |i| i.odd? }
=> [2,4]
sort
Sorting puts an array, or other enumerable, in order according its default sorting logic, or, if you wish to override that, a test that you define:
%w{dog cat pig fish}.sort => ["cat", "dog", "fish", "pig"]
# alphabetical
%w{dog cat pig fish}.sort { |a, b| a <=> b } => ["cat", "dog", "fish", "pig"]
# same thing. This is the default behavior spelled out in code.
%w{dog cat pig fish}.sort { |a, b| b <=> a } => ["pig", "fish", "dog", "cat"]
# here we've reversed the default behavior by switching the order of the *a and *b arguments in the comparison operation
%w{dog cat piglet fish}.sort { |a, b| a.length <=> b.length } => ["dog", "cat", "fish", "piglet"]
# here we're changing comparison to be based on the string's length
sort_by
Similar to sort, but instead of requiring you to use the comparison operator *<=>, sort_by sorts based on what each element in your enumerable would produce when passed to your block:
We could rewrite the last example above like:
%w{dog cat piglet fish}.sort_by { |word| word.length } => ["dog", "cat", "fish", "piglet"]
Note this comment in the docs:
> The result is not guaranteed to be stable. When two keys are equal, the order of the corresponding elements is unpredictable.
In the example abovedog
andcat
are the same length. In this case, it left them in the order they were in in the original array, but we can't rely on this behavior. In another circumstance,sort_by
might change the order of "equal" comparisons.
take
Sort of the opposite ofdrop
, returns the first n elements of the enumerable, where n is the argument passed totake
. If n is more than the length of the enumerable,take
just returns the original enumerable:
[1,2,3,4,5].take(3) => [1,2,3]
[1,2,3,4,5].take(10) => [1,2,3,4,5]
take_while
Sort of the opposite of *drop_while, returns everything up until but not including the first element that fails your test
[1, 2, 3, 4, 5].take_while { |i| i < 3 } #=> [1, 2]
reduce
The last thing we want to look at in this chapter is the reduce method. Reduce is one of those things that can be hard to grasp, but which can change the way you look at programming.
Like a lot of the methods here, *reduce executes your block one time for each element in the enumerable. The difference between reduce and many other iterators, though, is that it doesn't try to produce a new enumerable with a one-to-one correspondence with the old enumerable. Instead, as it iterates over the enumerable, *reduce accumulates the output of the block each time, passing the result of each calculation to the next block, along with the value of the current element. As a result, the block in *reduce has _two_ arguments.
[1,2,3,4,5].reduce { |accumulated_value, i| accumulated_value + i }
=> 15
you can specify what you want "accumulated_value" to be for the first iteration:
[1,2,3,4,5].reduce(10) { |accumulated_value, i| accumulated_value + i }
=> 25
If we were to spell out the calculations that happen in the last line, they would look like this:
accumulated_value = 10
accumulated_value = accumulated_value + 1
accumulated_value = accumulated_value + 2
accumulated_value = accumulated_value + 3
accumulated_value = accumulated_value + 4
accumulated_value = accumulated_value + 5
=> 25