RSS Log in
 

Annotated notes from reading on Enumerators of chapter ten (Collections central: Enumerable and Enumerator) of The Well-Grounded Rubyist by David A. Black.

 

An Enumerator is an object with an each method that is used as an Enumerable object.

The difference between an Enumerator and an Iterator is that an Enumerator is an enumerable object that can maintain state and therefore remembers where it is in the enumeration.

 

Enumerator.new

e = Enumerator.new do |y|
    puts "START"
    (1..3).each {|i| y << i}
    puts "END"
end

p e.map {|n| n * 10}

Output:

START
END
[10, 20, 30]

 

Note: y is an instance of Enumerator::Yielder and is automatically passed to the code block.

 

The above is equivalent to:

e = Enumerator.new do |y|
    y << 1
    y << 2
    y << 3
end

 

Enumerators can also be instantiated by blockless iterator calls:

e = "Hello".each_char
p e

e.each {|c| print c}

Output:

#<Enumerator: "Hello":each_char>
Hello

 

  • Kernel#enum_for

Enumerators can ‘attached’ themselves to other objects by specifying an enumerable method to hook up to. When an Enumerator each (or other enumerable) method is called, it will get the values to return by triggering the next yield from the given enumerable method of the object it is attached to.

countries = %w{Denmark Finland France Spain UK}

e = countries.enum_for(:select)

p e.each {|c| c.size > 5}

 

You can also pass arguments to the enum_for method if they are expected by the object’s target method itself:

numbers = [1, 2, 3, 4]

e = numbers.enum_for(:inject, 10)

e.each {|acc, n| acc + n}
>> 20

 

  • Kernel#to_enum

  • The to_enum method hooks up directly to a collection’s underlying each method:
  • hash = { "Bob" => "UK", "Bill" => "France" }
    
    e = hash.to_enum
    p e
    
    e.each {|key, value| puts "#{key} lives in #{value}."}

       

      Using Enumerators to protect collection from change

      If a method is passed a collection directly, the method is able to add or remove elements from the collection without problem.If however we want to prevent anyone from modifying the original collection, we can pass on an Enumerator which will allow for standard enumerable operations to take place (querying, filtering, etc.) but not change the collection.

      Accessing a collection using an Enumerator and not through the collection itself protects the collection from change.

       

      Here we have access directly to the internal collection and modify it:

      class Countries
          def names
              @names ||= %w{France Denmark}
          end  
      end
      
      countries = Countries.new
      
       countries.names << "UK"
       p countries.names
      
       >> ["France", "Denmark", "UK"]

       

      If we try to do the same using the Enumerator, we get an error:

      class Countries
          def names
              @names ||= %w{France Denmark}
          end  
      
          def immutable_names
              # Equivalent to:
              # Enumerator.new(names)
              self.names.to_enum
          end
      end
      
      
      countries = Countries.new
      
      countries.immutable_names << "UK"

      Output:

      undefined method `<<' for #<Enumerator: nil:each> (NoMethodError)

       

      But we can still however iterate through the collection:

      class Countries
          def names
              @names ||= %w{France Denmark}
          end  
      
          def immutable_names
              self.names.to_enum
          end
      end
      
      
      countries = Countries.new
      countries.immutable_names.each {|c| puts c.upcase}

      Output:

      FRANCE
      DENMARK

       

       

      Enumerator#next and Enumerator#rewind

      You can move through the collection going forward or backwards using the next and rewind methods respectively.

      names ||= %w{France Denmark UK Spain}
      
      e = names.to_enum
      
      puts e.next
      puts e.next
      puts e.rewind
      puts e.next

      Output:

      France
      Denmark
      #<Enumerator:0x757ec0>
      France

       

      Adding enumerability

      class Countries
          NAMES = %w{France UK}
      
          def list(str)
              NAMES.each {|c| yield str + c}
          end
      end
      
      countries = Countries.new
      e = countries.enum_for(:list, "Name: ")
      
      puts e.select {|c| c}

      Output:

      Name: France
      Name: UK

       

      Method chaining

      countries = %w{France UK Spain Denmark}
      
      puts countries.select {|c| c.size > 5}.map(&:upcase).join(", ")

      Output:

      FRANCE, DENMARK

       

      Enumerator#with_index

      countries = %w{France UK Spain Denmark}
      
      p countries.map.with_index {|c, i| "#{i}: #{c}"}

      Output:

      ["0: France", "1: UK", "2: Spain", "3: Denmark"]


      Comments are closed
      © Copyright 2012 TheBooleanFrog Powered by: BlogEngine.NET|Credits|Subscribe via RSS

      Follow

      twitter linkedin linkedin rss

      TheBooleanFrog

      Programming sticky notes and other distractions...