Freitag, 3. Mai 2013

Ruby iterators

Ruby iterators are very sweet. But sometimes some of them seem to be ignored. And there is only reason I can imagine: they are not known. Instead, "Enumerable#each" is used as the universal weapon. No it's not. It's worth to read the API of Enumerable before coding an odd iteration. This is about your reputation. Let's start.

Use Case: How to accumulate a collection of items?

Enumerable#inject and its alias Enumerable#reduce iterate over all items and applies a binary operation to a memo (which contains the accumulated temporary value). A simple accumulation:
puts [2,3,4,5].inject { |sum, i| sum += i }
If a block is passed like here, the first parameter is the memo and the second is item itself.
The result is:
14
You also can set an initial value for the memo (sum):
puts [2,3,4,5].inject(1) { |sum, i| sum += i }
and the result is:
15
There is also a shorter way of the same accumulation by passing a symbol instead of the block. Then the items are sent to that method (in this case "+") of memo. Take a look:
puts [2,3,4,5].inject(1, :+)
with the same result:
15

Use Case: How to copy a collection and manipulate its items?

Enumerable#collect and its alias Enumerable#map iterate over all items like Enumerable#inject does, but applies an operation to each item (and that's the important difference). For example I could want to increment every number:
numbers = [2,3,4,5]
puts numbers.collect { |number| number += 1 }.inspect
Therefore it returns a new collection containing the modified items:
[3, 4, 5, 6]

Please note that the original collection "numbers" still contains the original items.

Use Case: How to search a collection for the first proper item?

Enumerable#find finds the first item in a collection for which the block returns true. For example I would want to find the first even number in a collection of numbers:
puts [3,4,2,5,1].find { |number| number.even? }
and found:
4

Please note, that 2 is also even but wasn't picked, because its index in the collection is higher than the index of 2.

Use Case: How to search a collection for every proper item?

Enumerable#find_all and its alias Enumerable#select are kind of similar to Enumerable#find, but they find ALL items in a collection for which the block returns true. That's why both return an array.
The same example collection of names:
puts [3,4,2,5,1].find_all { |number| number.even? }.inspect
finds not only 2, but also 4:
[4, 2]

Use Case: How to search a collection for every improper item?

Enumerable#reject is opposite to Enumerable#find_all. It finds all items for which the block returns FALSE. To me it exists more for readability reasons. But in the context of the same numbers example, which I used before, it would look like:
puts [3,4,2,5,1].reject { |number| number.even? }.inspect
copies the original array without the even numbers:
[3, 5, 1]

Use Case: How to search a collection for the item having the minimum value?

Enumerable#min_by can be confused with Enumerable#find, since it seems to do the same job. But a closer look, highlights that Enumerable#min_by is a more concise iterator for just value comparison. The same "even number" example:
puts [3,4,2,5,1].min_by { |number| number % 2 }
This time I determine the first even number with the modulo-2-algorithm (Numeric#even? also does modulo 2 internally). The first number with result 0 (minimum number) is picked for return:
4
You definitely would go for Enumerable#find to find the first even number, but I'd like to mention another example to illustrate the concision of Enumerable#min_by:
puts %w(Bob Al Susan).min_by { |name| name.length }
finds the shortest name:
"Al"

Use Case: How to search a collection for the item having the maximum value?

Enumerable#max_by is the sibling of Enumerable#min_by. It finds the first item having the maximum value:
puts %w(Bob Al Susan).max_by { |name| name.length }
finds the longest name:
"Susan"

Use Case: How to search a collection for the items having the minimum or maximum value?

Enumerable#minmax_by is the third sibling in family. It returns an array with 2 elements. One for the item having the minimum value and one for item having the maximum value:
puts %w(Bob Al Susan).minmax_by { |name| name.length }
finds the shortest and the longest name:
["Al", "Susan"]

Use Case: How to select all items of a collection as long as they are proper?

Enumerable#take_while iterates over a copied collection and keeps every item, for which the block returns true. If the first item is not proper due to the return value of the block, the iteration stops and returns the result array.
The names example:
puts %w(Bob Al Alexander Susan).take_while { |name| name.length < 8 }.inspect
stops during the third iteration ("Alexander" consists of 9 letters) and returns:
["Bob", "Al"]
Please note, that the fourth item ("Susan") wasn't selected, though it would satisfy the condition. And that is the difference to Enumerable#find_all.

Use Case: How to remove all items in a collection as long as they are proper?

Enumerable#drop_while iterates over a copied collection and removes every item from the new array, for which the block returns true. If the first item is not proper due to the return value of the block, the iteration stops and returns the result array.
The names example:
puts %w(Bob Al Alexander Susan).drop_while { |name| name.length < 8 }.inspect
stops during the third iteration ("Alexander" consists of 9 letters) and returns:
["Alexander", "Susan"]

Supported by Ruby 1.9.3

Keine Kommentare:

Kommentar veröffentlichen