A couple of weeks ago I wrote about some of the handy methods that are part of the Enumerable module in Ruby. This week I thought I would talk about how you can create a class that has all these handy methods by using the Enumerable mixin in your class.
I wrote a simple linked list class to use as a demonstration. Linked lists are a chain of objects, usually called nodes. Each node points to the node after it. Here’s a picture where the nodes are represented as sets of two boxes. The first box in each pair holds the value. The second box points to the node after it. A box with a slash through it has value
If it helps think about a linked list as being a set of clues in a treasure hunt. You start at one clue, it has some value and points you to the next clue, and so on until you get to the end. My node class looks like this:
class Node attr_accessor :value, :next def initialize value @value = value @next = nil end end
Each node has a value. The initialize method sets the value when it creates a new node. Nodes also have a value called next which is the next node in the chain. New nodes have next set to nil since they are at the end of the chain.
The linked list class itself has two instance variables: head and tail. Head is the first value in the chain. Tail is the last value in the chain. There are also two methods
pop. My implementation is a queue, so push adds the new value to the end, and pop removes a value at the beginning of the linked list.
class LinkedList attr_accessor :head, :tail def initialize @head = nil @tail = nil end def push value n = Node.new value if @tail @tail.next = n else @head = n end @tail = n end def pop raise "Empty List" unless @head v = @head.value @head = @head.next v end end
This is a basic implementation. Some edge cases are not covered, but this is sufficient to demo enumerable.
The Enumerable Module
Enumerable is a module in the Ruby Standard Library. That sentence contains a lot of technical jargon, but it means that Enumerable is a set of functionality that works on almost any collection. Hash, Array, and Range include Enumerable which is why methods like
each_cons work on all of them. Enumerable is also included in classes like
IO allowing you to use methods like
reject on collections of files or on file contents.
To include Enumerable in your class, you must implement
each and put
include Enumerable in the class definition. The
each method must take a block and call that block for each successive element in the collection. For directories, that might mean each file the directory contains. For a range, each calls the block with each successive integer. Here’s the implementation of
each for the linked list class.
each method first gets the head node, calls the block passing that node in using yield, and then sets node to the next node before looping. The last node always has
nil as the next value, so the while condition ensures the loop stops once we have hit the end of the chain. That is all that you need to add all the methods of Enumerable in your class.
I can run
each_with_index on my linked list. I can use
each_slice. I can call
minmax on a linked list and get the minimum and maximum values stored in that list. I can even use one of my favorite methods,
zip, to interleave values from a linked list with values in another linked list or another collection class altogether.
When To Mixin Enumerable
As you have seen, it is easy to make a class enumerable. However, just because you can doesn’t mean you should. You should only include Enumerable if your class is a collection, not when it has a collection. For example, a classroom class will have a collection of students. It also will probably have attributes for the teacher’s name, the location, meeting, and a subject or grade. While it would be handy to call
classroom.each to iterate through the students, it is bad design to do so. You want to iterate through the students so
classroom.students.each is more accurate and will lead to more maintainable code in the long term.
As a comparison, if you were implementing a RecordSet class to represent rows of a database it might make sense to include Enumerable. In this case, the class is a kind of collection. It probably doesn’t have any additional attributes beyond the data. Since it is a collection that you can enumerate, it makes sense to include Enumerable. If you are having problems telling the difference between these cases, look to see what additional attributes are in the class. If there are several attributes in addition to the collection or data, then it probably isn’t a good idea to include Enumerable. If the primary attribute is the collection then including Enumerable may make sense.