module Enumerable

Practical examples and pitfalls for Enumerable

Enumerable examples: choose the result before choosing the method

Practical notes by Ruby-Doc.org

Establish your requirements before chaining

Establish what you wish to receive from the chained collection. Is it just one record that meets some criteria, all records meeting a certain criterion, one value per record, or a sum? A parcel weight summary dashboard looking at the weights of already prepared packages needs selection followed by aggregation. It does not need an array of incidental block return values.

Example 1
parcels = [
  {ready: true, kg: 4},
  {ready: false, kg: 9},
  {ready: true, kg: 6}
]
total = parcels.select { |parcel| parcel[:ready] }
               .sum { |parcel| parcel[:kg] }
p total
Expected output
10

Having the filter in place prior to calculating will allow the business rule to be easily modified. For example, if the dashboard were to separate packaged parcels from dispatched parcels, this modification should occur in the filter predicate. The calculation used to determine the total can remain entirely focused upon determining weight. In cases where there are no items to select, this example will automatically create a total of zero.

End the pipeline when sufficient results have been obtained

Lazy pipelines help in instances when the source is very large (e.g., a database) and/or unbounded, but only a few results are required. As such, the sequence will generate numbers until it generates three values which meet the filter criteria. The force method is then invoked to obtain the result. Generation of the lazy pipeline alone does not collect these values.

Example 2
matches = (1..).lazy.select { |n| n % 7 == 0 }
               .map { |n| "batch-#{n}" }
               .take(3).force
p matches
Expected output
["batch-7", "batch-14", "batch-21"]

Place the stop operator after the condition when the requirement is three matches. Generating the first three values before passing through the filter will be answering a different question than the report originally requested and possibly providing less data. Additionally, please consider how long a file or database resource will remain open while enumeration is deferred.

Watch operations which keep collections

While laziness may reduce memory usage for many collection operations, some operations (e.g., grouping every record or sorting every value) still need an overall result. If only counts per status are required in a report, tally, or a simple accumulator may provide a more direct expression than keeping the original records in grouped arrays.

When testing an existing reusable collection helper against new inputs, include both an empty input set as well as a single item. Next, ask yourself whether the receiver is safe to traverse again. Most arrays are safe to be traversed more than once; however, a stream's position may have changed since the first traversal. Make sure to write a clear single pass over the collection whenever possible so as to avoid multiple traversals resulting in duplicate data accesses or repeated costly work.

API reference: Enumerable API reference

Related: Array · Enumerator

What’s Here

Module Enumerable provides methods that are useful to a collection class for:

Methods for Querying

These methods return information about the Enumerable other than the elements themselves:

Methods for Fetching

These methods return entries from the Enumerable, without modifying it:

Leading, trailing, or all elements:

Minimum and maximum value elements:

Groups, slices, and partitions:

Methods for Searching and Filtering

These methods return elements that meet a specified criterion:

Methods for Sorting

These methods return elements in sorted order:

Methods for Iterating

Other Methods

Usage

To use module Enumerable in a collection class:

Example:

class Foo
  include Enumerable
  def each
    yield 1
    yield 1, 2
    yield
  end
end
Foo.new.each_entry{ |element| p element }

Output:

1
[1, 2]
nil

Enumerable in Ruby Classes

These Ruby core classes include (or extend) Enumerable:

These Ruby standard library classes include Enumerable:

Virtually all methods in Enumerable call method each in the including class:

About the Examples

The example code snippets for the Enumerable methods:

Extended Methods

A Enumerable class may define extended methods. This section describes the standard behavior of extension methods for reference purposes.

size

Enumerator has a size method. It uses the size function argument passed to Enumerator.new.

e = Enumerator.new(-> { 3 }) {|y| p y; y.yield :a; y.yield :b; y.yield :c; :z }
p e.size #=> 3
p e.next #=> :a
p e.next #=> :b
p e.next #=> :c
begin
  e.next
rescue StopIteration
  p $!.result #=> :z
end

The result of the size function should represent the number of iterations (i.e., the number of times Enumerator::Yielder#yield is called). In the above example, the block calls yield three times, and the size function, +-> { 3 }+, returns 3 accordingly. The result of the size function can be an integer, Float::INFINITY, or nil. An integer means the exact number of times yield will be called, as shown above. Float::INFINITY indicates an infinite number of yield calls. nil means the number of yield calls is difficult or impossible to determine.

Many iteration methods return an Enumerator object with an appropriate size function if no block is given.

Examples:

["a", "b", "c"].each.size #=> 3
{a: "x", b: "y", c: "z"}.each.size #=> 3
(0..20).to_a.permutation.size #=> 51090942171709440000
loop.size #=> Float::INFINITY
(1..100).drop_while.size #=> nil  # size depends on the block's behavior
STDIN.each.size #=> nil # cannot be computed without consuming input
File.open("/etc/resolv.conf").each.size #=> nil # cannot be computed without reading the file

The behavior of size for Range-based enumerators depends on the begin element:

Examples:

(10..42).each.size #=> 33
(10..42.9).each.size #=> 33 (the #end element may be a non-integer numeric)
(10..).each.size #=> Float::INFINITY
("a".."z").each.size #=> nil
("a"..).each.size #=> nil
(1.0..9.0).each.size # raises TypeError (Float does not have #succ)
(..10).each.size # raises TypeError (beginless range has nil as its #begin)

The Enumerable module itself does not define a size method. A class that includes Enumerable may define its own size method. It is recommended that such a size method be consistent with Enumerator#size.

Array and Hash implement size and return values consistent with Enumerator#size. IO and Dir do not define size, which is also consistent because the corresponding enumerator’s size function returns nil.

However, it is not strictly required for a class’s size method to match Enumerator#size. For example, File#size returns the number of bytes in the file, not the number of lines.