The Well Grounded Rubyist - Part 10

Collections central: Enumerable and Enumerator

  • in Ruby, common characteristics amount many objects tend to reside in modules
  • collection objects in Ruby typically include the Enumerable module
  • by itself, the Enumerable module doesn't do much. To tap into the benefits of Enumerable, you must define an each instance method in your class

in some respects, you might say the whole concept of a “collection” in Ruby is pegged to the Enumerable module and the methods it defines on top of each

Gaining enumerability through each

Any class that aspires to be enumerable must have an each method whose job is to yield items to a supplied code block, one at a time

  • however each is implemented, the methods in the Enumerable module depend on being able to call it
Collection type
Implementation of each
Arrays each yields the first element, then second, and so on
Hashes each yields key/value pairs in the form of two-element arrays
file handle each yields one line of the file at a time
Ranges first deciding whether iteration is possible and then pretends to be an array. Ranges can't iterate if the starting number is a float
Your own classes `each` is whatever you want it to be, as long as you yield something

find

  • the find method returns the first element in an enumerable object for which the code block provided returns true. For instance, in a Rainbow class, you can find the first colour that begins with the letter "y"
r = Rainbow.new  
y_color = r.find { |color| color.start_with?('y') }  
puts "First colour starting with 'y' is #{y_color}."  
  • find works by calling each
  • each yields items, and find uses the code block we've given it to test those items one at a time for a match
  • there's no need to define find, it is part of Enumerable
  • to know which methods Enumerable provides, you can run
Enumerable.instance_methods(false).sort  
  • all these methods are built on top of each
  • sometimes collection classes (Array, Hash, etc) overwrite some of the Enumerable methods

Enumerable Boolean queries

  • a number of Enumerable methods return true or false depending on whether one or more elements match certain criteria
# given a rainbow array, these methods will return true or false:

rainbow.include?("red")

rainbow.all? { |colour| colour =~ / / }

rainbow.any? { |colour| colour =~ / / }

states.one? { |colour| colour =~ /indigo/ }

states.none? { |colour| colour =~ /black/ }  
  • if rainbow were a hash, you could run similar tests on it, but you'd have to account for the key/value pairs
  • Hash#include? method checks for key inclusion, but the other methods handle key/value pairs
# given a rainbow hash, these methods will return true or false

rainbow.include?("orange")

rainbow.all? { |colour, abbrev| colour =~ /yellow/ }

rainbow.one? { |colour, abbrev| colour =~ /black/ }

# you can use just the key
rainbow.keys.all? { |colour, abbrev| colour =~ / / }  
  • set iteration works much like array iteration for Boolean query purposes
  • if rainbow were a set, you can run all the exact same queries as the ones in the array example and get the same results
  • enumerability with ranges is a little trickier
  • the include? method works for any range
  • the methods work if the range contains only integers, but once it is a range of floats, (or a range whose start number is a float) then calling any of the methods triggers a fatal error
Hashes iterate with two-element arrays

When you iterate through a hash with each or any other built-in iterator, the hash is yielded to your code block one key/value pair at a time—and the pairs are two-element arrays. You can, if you wish, provide just one block parameter and capture the whole little array: hash.each {|pair| ... }

In such a case, you’ll find the key at pair[0] and the value at pair[1]. Normally, it makes more sense to grab the key and value in separate block parameters. But all that’s happening is that the two are wrapped up in a two-element array, and that array is yielded. If you want to operate on the data in that form, you may.

Enumerable searching and selecting

  • the Enumerable module provides several facilities for filtering collections and for searching collections to find one or more elements that match one or more criteria
  • all the search and select methods are iterators; they expect a code block

Get first match with find

  • find is also available as detect
  • it locates the first element in an array for which the code block returns true
  • if find fails to find an element that passes the code-block test, it returns nil
  • so if the array actually contains elements that are nil, the find test will always return nil, in which case, the test is useless
[1,2,3,4,5,6,7,8,9,10].find {|n| n > 5 } # => 6

[1,2,3,nil,4,5,6].find {|n| n.nil? }
  • to work around the nil issue, you can use a method like include? to see whether or not the array contains elements that are nil
  • another work around is to provide a "nothing found" function (which is a Proc object) as an argument to find. This "nothing found" method will be called if the find operation fails
failure = lambda { 11 }  
over_ten = [1,2,3,4,5,6].find(failure) {|n| n > 10 } # => 11  
The dominance of the array

Arrays serve generically as the containers for most of the results that come back from enumerable selecting and filtering operations, whether or not the object being selected from or filtered is an array.

... The array is the most generic container and therefore the logical candidate for the role of universal result format.

A few exceptions arise. A hash returns a hash from a select or reject operation. Sets return arrays from map, but you can call map! on a set to change the elements of the set in place. For the most part, though, enumer- able selection and filtering operations come back to you inside arrays.

Get all matches with find_all (a.k.a select) and reject

  • find_all (also available as select) returns a new collection containing all the elements of the original collection that match the criteria in the code block, not just the first matching element (as with find)
  • if there are no matches found, find_all returns an empty collection object
  • Ruby makes special arrangements for hashes and sets, though. If you select on a hash or a set, you get back a hash or set
  • arrays, hashes, and sets have a bang version of select (select!) that reduces the collection permanently to only those elements that passed the selection test
  • there is no find_all! bang version, you have to use select!
a = [1,2,3,4,5,6,7,8,9,10]

a.find_all {|item| item > 5 } #  => [6, 7, 8, 9, 10]  
a.select {|item| item > 100 } # => []  
  • using reject, you can find out which elements of an array do not return a true value when yielding to the block
  • there is a bang, in-place version reject!, specifically for arrays, hashes and sets
a.reject {|item| item > 5 } # => [1, 2, 3, 4, 5]  

Selecting on threequal matches with grep

  • Enumerable#grep method lets you select from an enumerable object based on the case equality operator, ===
  • the most common application of grep is the one that corresponds most closely to the common operation of the command-line utility of the same name - pattern matching for strings
colours = %w{ red orange yellow green blue indigo violet }  
colours.grep(/o/) # => ["orange", "yellow", "indigo", "violet"]

miscellany = [75, "hello", 10...20, "goodbye"]  
miscellany.grep(String) # => ["hello", "goodbye"]  
miscellany.grep(50..100) # => [75]  
  • in general, the statement enumerable.grep(expression) is functionally equivalent to
  • it selects for a truth value based on calling ===
enumerable.select {|element| expression === element }  
  • grep method can take a block, in which case it yields each elemtn of its result set to the block before returning the results
colours = %w{ red orange yellow green blue indigo violet }  
colours.grep(/o/) { |colour| colour.capitalize } # => ["Orange", "Yellow", "Indigo", "Violet"]  

Organising selection results with group_by and partition

  • a group_by operation on an enumerable object takes a block and returns a hash
colours = %w{ red orange yellow green blue indigo violet }  
colours.group_by { |colour| colour.size } # => {3=>["red"], 6=>["orange", "yellow", "indigo", "violet"], 5=>["green"], 4=>["blue"]}  
  • the partition method is similar to group_by but it splits the elements of the enumerable into two arrays based on whether the code block returns true for the element
colours = %w{ red orange yellow green blue indigo violet }  
colours.partition { |colour| colour.include?('o') } # => [["orange", "yellow", "indigo", "violet"], ["red", "green", "blue"]]  

Element-wise enumerable operations

  • collections contain special-status individual objects:
    • the first in the collection
    • the last in the collection
    • the greatest (largest)
    • the least (smallest)

the first method

  • Enumerable#first returns the first item encountered when iterating over the enumerable
  • the object returned by the first method is the first object yielded by each
[1,2,3,4].first # => 1
(1..10).first # => 1
{1 => 2, "one" => "two"}.first # => [1, 2]
  • taking the first element of a hash gives you a two-element array containing the first pair that was inserted into the hash
  • assigning a new value to the element doesn't change the insertion order
  • the most noteworthy point about Enumerable#first is that there's no Enumerable#last. This is because finding the end of the iteration isn't as straightforward as finding the beginning. Imagine if an iteration went on forever
hash = { 3 => "three", 1 => "one", 2 => "two" }  
hash.first # => [3, "three"]

hash[3] = "trois"  
hash.first # => [3, "trois"]  
  • some enumerable classes, like Array and Range do have a last method

The take and drop methods

  • when you take elements, you get those elements
  • when you drop elements, you get the original collection minus the elements you've dropped
  • you can constrain the take and drop operations by providing a block and using the variant forms take_while and drop_while, which determine the size of the "take" not by an integer argument but by the truth value of the block
  • the take and drop operations are a kind of hybrid of first and select. They are anchored to the beginnig of the iteration and terminate once they've satisfied the quantity requirement or encountered a block failure
states = %w{ NJ NY CT MA VT FL }  
states.take(2) # => ["NJ", "NY"]  
states.drop(2) # => ["CT", "MA", "VT", "FL"]

states.take_while {|s| /N/.match(s) } # => ["NJ", "NY"]  
states.drop_while {|s| /N/.match(s) } # => ["CT", "MA", "VT", "FL"]  

The min and max methods

  • minimum and maximum are determined by the <=> (spaceship comparison operator) logic
  • if you want to perform a minimum or maximum test based on non-default criteria, you can provide a code block
# default behaviour
[1,3,5,4,2].max
%w{ Ruby C APL Perl Smalltalk }.min # => "APL"

# non-default with block
%w{ Ruby C APL Perl Smalltalk }.min {|a,b| a.size <=> b.size } # => "C"
  • a more streamlined block-based approach is to use min_by and max_by, which perform the comparison implicitly
%w{ Ruby C APL Perl Smalltalk }.min_by {|lang| lang.size } # => "C"
  • there is also a minmax method (and the corresponding minmax_by method) which gives you a pair of values, one for the minimum and one for the maximum
%w{ Ruby C APL Perl Smalltalk }.minmax # => ["APL", "Smalltalk"]
%w{ Ruby C APL Perl Smalltalk }.minmax_by {|lang| lang.size } # => ["C", "Smalltalk"]
  • the min/max family of enumerable methods is always available, even when it isn't a good idea to use them. If you tried to find the max of an infinite list of enumerables, your program would hang, because it would never allow a maximum value to be determined

  • for hashes, min and max use the keys to determine ordering. If you want to use values, the *_ members of the min/max family can help you

state_hash = {"New York"=>"NY", "Maine"=>"ME", "Alaska"=>"AK", "Alabama"=>"AL"}

# minimum pair, by key
state_hash.min # => ["Alabama", "AL"]

# minimum pair, by key
state_hash.min_by { |name, abbr| name } # => ["Alabama", "AL"]

# minimum pair, by value
state_hash.min_by { |name, abbr| abbr } # => ["Alaska", "AK"]  

Relatives of each

  • Enumerable makes several methods available that are similar to each
  • these other methods behave like each, in that they go through the whole collection and yield elements from it and stopping only when they've gone all the way through
  • these methods include
    • reverse_each
    • each_with_index
    • each_slice
    • each_cons
    • cycle
    • inject

reverse_each

  • this method iterates backwards through an enumerable
  • be careful not to use reverse_each on an infinite iterator, since you'd need to know the last element if you want to reverse the enumerable (infinite loops may ensue)
[1,2,3].reverse_each { |e| puts e * 10 }

each_with_index method (and each.with_index)

  • each_with_index differs from each in that it returns an additional integer that represents the ordinal position of the item
  • this index can be useful for labeling objects, amount other purposes
fruits = %w[apple orange pear grapes]  
fruits.each_with_index { |fruit, i| puts "#{i + 1}. #{fruit}" }  
  • there is an anomaly that is involved in each_with_index method. Every enumerable object has the each_with_index method, but not every enumerable object has knowledge of what an index is
  • you can see this by asking enumerables to perform an each_index (as opposed to each_with_index operation
  • arrays have a fundamental sense of an index
  • hashes do not have a fundamental sense of an index. Although, they do have a sense of with index
  • it is unusual to perform an each_with_index operation on hashes, though
%w{a b c }.each_index {|i| puts i }

letters = {"a" => "ay", "b" => "bee", "c" => "see" }  
letters.each_with_index { |(key, value), i| puts i }

letters.each_index { |(key, value), i| puts i } # => NoMethodError  

- Enumerable#each_with_index works, but it is somewhat deprecated. Instead consider using #with_index method of the enumerator you get back from calling each

array = %w{ red yellow blue }

array.each.with_index { |colour, i| puts "#{i}. #{colour}"}  
  • using each_index also buys you some functionality: you can provide an argument that will be used as the first index value, thus avoiding the need to add one to the index like we did previously
array = %w{ red yellow blue }

array.each.with_index { |c, i| puts "#{i}. #{c}" }  

The each_slice and each_cons methods

  • each_slice and each_cons are specialisations of the each method that walk through a collection a certain number of elements at a time
  • each_slice and each_cons yield an array of that many elements to the block on each iteration
  • each_slice handles each element only once
  • each_slide yields collections progressively in slies of size n (or less than n, if fewer than n elements remain)
  • each_cons takes a new grouping at each element and thus produces overlapping yielded arrays
  • each_cons moves through the collection one element at a time, and at each point yields an array of n elements, stopping when the last element in the collection has been yielded once
array = [1,2,3,4,5,6,7,8,9,10]  
array.each_slice(3) { |slice| p slice }  
# => [1, 2, 3]
# => [4, 5, 6]
# => [7, 8, 9]
# => [10]

array.each_cons(3) { |cons| p cons }  
# => [1, 2, 3]
# => [2, 3, 4]
# => [3, 4, 5]
# => [4, 5, 6]
# => [5, 6, 7]
# => [6, 7, 8]
# => [7, 8, 9]
# => [8, 9, 10]

The cycle method

  • Enumerable#cycle yields all the elements in the object again and again in a loop
  • if you provide an integer argument, the loop will be run that many times
  • if you don't provide an integer argument, the loop will be run forever
  • Page 303

Enumerable reduction with inject

  • the inject (a.k.a reduce) method works by initializing an accumulator object and then iterating through the collection (an enumerable object), performing a calculation on each iteration and resetting the accumulator, for purposes of the next iteration, to the result of that calculation
  • the classic example of injecting is the summing up of numbers n an array
[1,2,3,4].inject(0) {|acc,n| acc + n } # => 10
  • if you don't supply an argument to inject, it uses the first element in the enumerable object as the initial value of acc

The map method

  • the map method (also callable as collect)
  • whatever enumerable it starts with, map always returns an array
  • the returned array is always the same size as the original enumerator
  • the new array is the same size as the original array, and each of its elements corresponds to the element in the same position in the original array. But each element has been run through the block
names = %w{ David Yukihiro Chad Amy }  
names.map { |name| name.upcase } # => ["DAVID", "YUKIHIRO", "CHAD", "AMY"]  

Using a symbol argument as a block

  • you could rewrite the previous example this way
names.map(&:upcase)  

The return value of map

  • the return value of each doesn't matter because each returns its receiver
  • on the other hand, map returns a new object, mapping the original object to a new object
  • this difference between each and map is a good reminder that each exists purely for the side effects from the execution of the block
Be careful with block evaluation
  • the return value of puts is always nil
  • in the example, when using puts, the code in the block is being executed therefore five values represented by n * 100 will be printed to the screen. But the returned value is what the puts operation returns
array = [1,2,3,4,5]  
result1 = array.map { |n| puts n* 100 } # => [nil, nil, nil, nil, nil]

result2 = array.map { |n| n * 100 } # => [100, 200, 300, 400, 500]  

In-place mapping with map!

  • there is an in-place version of map for arrays and sets: map! (a.k.a. collect)

Strings as quasi-enumerables

  • you can iterate through raw bytes or the characters of a string using methats that treat the string as a collection of bytes, characters, code points, or lines
  • the 4 mthos of iteratring through a sting have an each-style method associated with it

  • each_byte can be used to iterate through bytes

str = "abcde"  
str.each_byte { |b| p b }  
 # => 97
 # => 98
 # => 99
 # => 100
 # => 101

-if you want each character, rather than it's byte code, use each_char

str = "abcde"  
str.each_char { |c| p c }  
 # => "a"
 # => "b"
 # => "c"
 # => "d"
 # => "e"
  • iterating by code point provide character codes (integers) at the rate of exactly one per character
str = "100\u20ac" # => "100€"  
str.each_codepoint { |cp| p cp }  
  • due to the encoding, the number of bytes is greater than the number of code points
str = "100\u20ac"  
str.each_codepoint {|cp| p cp }

str.each_byte {|b| p b }  
  • if you want to iterate line by line, use each_line
str = "This string\nhas three\nlines"  
str.each_line {|l| puts "Next line: #{l}" }  
  • the string above is split at the end of each line, or more strictly speaking, at every occurrence of the current value of the global variable $/.
  • if you change the global variable, you're changing the delimiter for what Ruby considers the next line in a string
  • in the example, the concept of "line" will be based on the ! character
str = "David!Alan!Black"  
$/ = "!"
str.each_line {|l| puts "Next line: #{l}" }  
 # => Next line: David!
 # => Next line: Alan!
 # => Next line: Black

Sorting enumerables

  • if you have a class and want to be able to arrange multiple instances of it in order, you need to do the following:
  • define a comparison method for the class (<=>)
  • place the multiple instances in a container, probably an array
  • sort the container

  • although the ability to sort is granted by Enumerable, your class doesn't have to mix in Enumerable. Rather, you put your objects into a container object that does mix in Enumerable

  • the container object, as an enumerablehas two sorting methods:sortandsort_by`
  • sorting numbers or strings is pretty straightforward because numbers and strings have some knowledge of what it means to be in order
  • for instance, if you wanted to sort an array of paintings, you wouldn't be able to simply use the sort method. You would need to give the paintings some knowledge of what it means to be greater or less than something. You do this by defining the spaceship operator: Painting#<=> find

  • a more fleshed-out account of the steps involved might look like:

  • teach your objects how to compare themselves with each other, using <=>
  • put those objects inside an enumerable object (probably an array)
  • ask that object to sort itself. It does this by asking the objects to compare themselves with each other using <=>

Where the Comparable module fits into enumerable sorting (or doesn’t)

the <=> method is useful both for classes whose instances you wish to sort and for classes whose instances you wish to compare with each other in a more fine-grained way using the fill complement of comparison operators

  • there are several techniques when comparing
    • 1) if you define <=> for a class, then instances of that class can be put inside an array or other enumerable for sorting
    • 2) if you don't define <=>, you can still sort objects if you put them inside an array and provide a code block with instructions on how to rank any two objects
    • 3) if you define <=> and also include Comparable, you can sort and perform all the comparison operators

Defining sort-order logic with a block

  • if the <=> has not been defined, you can supply a block to indicate how you want your objects sorted
  • if the <=> is defined, you can override it for the current sort operation by providing a block, for example: nil
  • sort with a block can help where the existing comparison methods won't get the job done

Concise sorting with sort_by

  • like sort, sort_by is an instance method of Enumerable
  • the main difference between sort and sort_by is that sort_by always takes a block, and it only requires that you show it how to treat one item in the collection
  • sort_by figures out that you want to do the same thing to both items every time it compares a pair of objects
    [1,2,3,4,5,6,7,8,9,10].find {|n| n > 5 } # => 6
    
    [1,2,3,nil,4,5,6].find {|n| n.nil? }
    
  • all we have to do in the block is show (once) what action needs to be performed to prep each object for the sort operation
  • we don't have to call to_i on two objects, nor do we need to use the <=> method explicitly

Enumerators and the next dimension of enumerability

  • enumerators are closely related to iterators, bu the aren't the same thing
  • an iterator is a method that yields one or more values to a code block
  • an enumerator is an object, not a method
  • an enumerator is a simple enumerable object. It has an each method and it employs the Enumerable module to define all the usual methods - select, inject, map, etc - directly on top of its each
  • an enumerator is not a container object, therefore it has no "natural" basis for an each operation, the way an array does. The each iteration logic of every enumerator has to be explicitly specified
  • after you explicitly specify how to do each, the enumerator takes over and figures out how to do map, find, take, drop and all the rest
  • there are two main techniques for hooking up the each method in an enumerable
    • 1) call Enumerator.new with a code block that contains each logic
    • 2) create an enumerator based on an existing enumerable object so that it draws its logic from the specific enumerable object it is based on

Creating enumerators with a code block

In the following example

  • y is a yielder, an instance of Enumerator::Yielder, that is automatically passed to your block
  • yielders encapsulate the yielding scenario you want your enumerator to follow
  • in this example, we're saying "when the enumerator gets an each call, take that to mean you should yield 1, then 2, then 3"
  • the << method serves to instruct the yielder as to what it should yield nil

  • the enumerator iterates once for every time that << (or the yield method) is called on the yielder

  • if you put calls to << inside a loop or other iterator inside the code block, you can introduce just about any iteration logic you want
  • the above example can be rewritten as follows include?

  • you can involve other objects in the code block of the enumerator nil

Attaching enumerators to other objects

  • another way to give an enumerator each logic is to hook it up to another object, specifically to an iterator on another object
  • when it needs to yield something, it gets the necessary value by triggering the next yield from the object to which it is attached, via the iterator method
  • for this approach, we can use enum_for (a.k.a to_enum) on the object from which you want the enumerator to draw its iterations
  • the first argument will be the name of the method onto which the enumerator will attach its each method
  • it will default to :each, but it doesn't have to be the each method, it can be a different method Proc
  • that means the enumerator's (e) each will serve as a kind of front end to array's select find

  • you can also provide arguments for enum_for. These provided arguments are passed through to the method to which the enumerator is being attached

  • below is an example to create an enumerator for the inject method, so that when inject is called to feed values to the enumerator's each, it is called with a starting value of "Names: " find
  • the enumerator, e, in the above example is permanently changed (this can be seen by running this line again and again
    failure = lambda { 11 }  
    over_ten = [1,2,3,4,5,6].find(failure) {|n| n > 10 } # => 11  
    

Implicit creation of enumerators by blockless iterator calls

  • an iterator is a method that yields one or more values to a block
  • most built-in iterators return an enumerator when they're called without a block find_all

Enumerator semantics and uses

How to use an enumerator's each method

  • an enumerator's each method is hooked up to a method on another object. This method on another object may possibly be a method other than each
  • if you use it directly, it behaves like that other method, including with respect to its return value

select - the enumerator is not the same object as the array, it has its own ideas about what each means

The un-overriding phenomenon
  • page 317

    if a class defines each and include Enumerable, its instances automatically get map, select, inject and all the rest of Enumerable's methods. All those methods are defined in terms of each

  • sometimes, though, a given class has overridden Enumerable's version of each. E.g. Out of the box, select method from Enumerable always returns an array. But a select operation on a hash returns a hash

  • if we hook up an enumerator to the select method, it gives us an each method that works like the select method
  • if you do not pass in an argument, when using to_enum, it defaults to using each find_all

Protecting objects with enumerators

  • if you pass an array as an argument to a method, the method can alter the array object
  • if you want to protect the array from change, you could duplicate it and pass on the duplicate of you can pass along an enumerator instead
  • the enumerator will allow for iterations through the array, but it won't absorb changes select
  • the following example shows how a card array can be protected find

Fine-grained iteration with enumerators

  • enumerators maintain state: they keep track of where they are in their enumeration
  • the enumerator allows you to move in slow motion , using next and rewind methods find_all
  • an enumerator is an object (an enumerable object) and therefore maintains state; it remembers where it is in the enumeration. An iterator is a method. An iterator doesn't have state

Adding enumerability with an enumerator

  • an enumerator can add enumerability to objects that don't have it. If you hook up an enumerator's each method to any iterator, you can use the enumerator to perform enumerable operations on the object that owns the iterator, whether that object considers itself enumerable or not

  • attaching an enumerator to a non-enumerator object is a good exercise because it illustrates the difference between the original object and the enumerator so sharply

Enumerator method chaining

  • method chaining is a common technique in Ruby programming, in part because it's so easy
  • the example below prints out a comma-separated list of uppercase names beginning with A through N select
  • the price of this chaining is the creation of intermediate objects. A new array is created as an output of select and another as an output of map, and a string is created as an output of join

  • Enumerators don't solve all the problems of method chaining, but they do mitigate the problem of creating intermediate objects in some cases

Economising on intermediate objects

  • many methods from the Enumerable module return an enumerator when they are called without a block
  • Enumerable methods that take arguments and return enumerators, like each_slice doesn't have to create an entire array of two-element slices in memory, rather it can create slices as they're needed by the map operation select

Indexing enumerables with with_index

  • Enumerators have a with_index method that adds numerical indexing, as a second block parameter, to any enumeration select!
  • you can chain with_index with other enumerator methods, so a method that is super specific like map_with_index is not needed

Exclusive-or operations on strings with enumerators

  • run the following example
  • add the XOR(exclusive-or) method to the string class, first find_all!

Lazy enumerators

  • lazy enumerators make it easy to enumerate selectively over infinitely large collections
  • in the following example, the code runs forever. The select operation never finishes, so the chained-on first command never gets executed select!

  • you can get a finite result from an infinite collection by using a lazy enumerator over that range:

    a = [1,2,3,4,5,6,7,8,9,10]
    
    a.find_all {|item| item > 5 } #  => [6, 7, 8, 9, 10]  
    a.select {|item| item > 100 } # => []  
    

  • you can wire this lazy enumerator up to select, creating a cascade of lazy enumerators reject
  • when you are lazy enumerating, it is possible to grab result sets from our operations without waiting for the completion of infinite tasks. i.e. we can now ask for the first 10 results from the select test on the infinite list and the infinite list is happy to enumerate only as much as is necessary to produce those 10 results reject!

  • As a variation on the same theme, you can create the lazy select enumerator and then use take on it

  • this allows you to choose how many multiples of 3 you want to see without hard-coding the number beforehand
  • note that you have to call force on the result of take; otherwise, you’ll end up with yet another lazy enumerator, rather than an actual result set
  • run the following code to see example
    a.reject {|item| item > 5 } # => [1, 2, 3, 4, 5]  
    

FizzBuzz with a lazy enumerator

  • FizzBuzz problem involves printing out integers from 1 to 100, following the rules:

    • if the number is divisible by 15, print "FizzBuzz".
    • else if the number is divisible by 3, print "Fizz".
    • else if the number is divisible by 5, print "Buzz".
    • else print the number.
  • the following is code uses a lazy enumerator to write a version of FizzBuzz that can handle any range of numbers Enumerable#grep

  • calling p fb(15) will result in
["1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8", "Fizz", "Buzz", "11",
     "Fizz", "13", "14", "FizzBuzz"]
  • without creating a lazy enumerator on the range, the map operation would go on forever. Instead, the lazy enumerator ensures the whole process stops once we've got what we want