Search code examples
rubyarrayscollectionseachextend

Ruby custom iterators


I have a class game which contains some arrays of custom objects (dinosaurs, cacemen etc.), that are returned by different accessors, such as game.dinosaurs, game.cavemen etc.

At present, all these accessors just return the internally stored arrays. But now I'd like to add some custom iteration methods to these arrays returned by those accessors, to be able to write code such as game.dinosaurs.each_carnivore { ... } etc. similarly to each_element and each_attr iterators in LibXML::XML::Node. But the objects returned from my accessors game.dinosaurs and game.cavemen have to behave like arrays still.

How are things like that usually done in Ruby? Should I make the objects returned from my accessors to be some custom classes derived from Ruby's Array class? Or maybe should I just create a custom class with Enumerable mixed in?

I know I can use map or select externally on my collections, but I wanted to encapsulate these iterations internally that my class's users won't need to bother how to set up an iteration to select only carnivore dinosaurs from the internal array.

Edit: I'm not asking about how to use iterators or how to implement them, but how to add just some custom iterators to object which previously were just plain arrays (and still need to be).


Solution

  • It depends (as always). You could use an array subclass and you you could build a custom class and use composition and delegation. Here's a simple example with an array subclass:

    class DinosaurArray < Array
      def carnivores
        select { |dinosaur| dinosaur.type == :carnivore }
      end
    
      def herbivores
        select { |dinosaur| dinosaur.type == :herbivore }
      end
    
      def each_carnivore(&block)
        carnivores.each(&block)
      end
    
      def each_herbivore(&block)
        herbivores.each(&block)
      end
    end
    

    And here's a simple one with composition and delegation:

    class DinosaurArray
      def initialize
        @array = []
      end
    
      def <<(dinosaur)
        @array << dinosaur
      end
    
      def carnivores
        @array.select { |dinosaur| dinosaur.type == :carnivore }
      end
    
      def herbivores
        @array.select { |dinosaur| dinosaur.type == :herbivore }
      end
    
      def each(&block)
        @array.each(&block)
      end
    
      def each_carnivore(&block)
        carnivores.each(&block)
      end
    
      def each_herbivore(&block)
        herbivores.each(&block)
      end
    end
    

    Both implementation can be used like this:

    require 'ostruct'
    
    dinosaurs = DinosaurArray.new
    dinosaurs << OpenStruct.new(type: :carnivore, name: "Tyrannosaurus")
    dinosaurs << OpenStruct.new(type: :carnivore, name: "Allosaurus")
    dinosaurs << OpenStruct.new(type: :herbivore, name: "Apatosaurus")
    
    puts "Dinosaurs:"
    dinosaurs.each.with_index(1) { |dinosaur, i| puts "#{i}. #{dinosaur.name}" }
    puts
    

    But also has custom iterators:

    puts "Carnivores:"
    dinosaurs.each_carnivore.with_index(1) { |dinosaur, i| puts "#{i}. #{dinosaur.name}" }
    puts
    
    puts "Herbivores:"
    dinosaurs.each_herbivore.with_index(1) { |dinosaur, i| puts "#{i}. #{dinosaur.name}" }
    

    Output:

    Dinosaurs:
    1. Tyrannosaurus
    2. Allosaurus
    3. Apatosaurus
    
    Carnivores:
    1. Tyrannosaurus
    2. Allosaurus
    
    Herbivores:
    1. Apatosaurus