Implementing Enumerable: Write One Method, Get 22 Free






Implementing Enumerable: Write One Method, Get 22 Free

Problem

You want to give a class all the useful iterator and iteration-related features of Ruby's arrays (sort, detect, inject, and so on), but your class can't be a subclass of Array. You don't want to define all those methods yourself.

Solution

Implement an each method, then include the Enumerable module. It defines 22 of the most useful iteration methods in terms of the each implementation you provide.

Here's a class that keeps multiple arrays under the covers. By defining each, it can expose a large interface that lets the user treat it like a single array:

	class MultiArray
	  include Enumerable

	  def initialize(*arrays)
	    @arrays = arrays
	  end

	  def each
	    @arrays.each { |a| a.each { |x| yield x } }
	  end
	end

	ma = MultiArray.new([1, 2], [3], [4])
	ma.collect                                        # => [1, 2, 3, 4]
	ma.detect { |x| x > 3 }                        # => 4
	ma.map { |x| x ** 2 }                             # => [1, 4, 9, 16]
	ma.each_with_index { |x, i| puts "Element #{i} is #{x}" }
	# Element 0 is 1
	# Element 1 is 2
	# Element 2 is 3
	# Element 3 is 4

Discussion

The Enumerable module is the most common mixin module. It lets you add a lot of behavior to your class for a little investment. Since Ruby relies so heavily on iterator methods, and almost every data structure can be iterated over in some way, it's no wonder that so many of the classes in Ruby's standard library include Enumerable: Dir, Hash, Range, and String, just to name a few.

Here's the complete list of methods you can get by including Enumerable. Many of them are described elsewhere in this book, especially in Chapter 4. Perhaps the most useful are collect, inject, find_all, and sort_by.

	Enumerable.instance_methods.sort
	# => ["all?", "any?", "collect", "detect", "each_with_index", "entries",
	# => "find", "find_all", "grep", "include?", "inject", "map", "max",
	# => "member?", "min", "partition", "reject", "select", "sort", "sort_by",
	# => "to_a", "zip"]

Although you can get all these methods simply by implementing an each method, some of the methods won't work unless your each implementation returns objects that can be compared to each other. For example, a data structure that contains both numbers and strings can't be sorted, since it makes no sense to compare a number to a string:

	ma.sort                                   # => [1, 2, 3, 4]
	mixed_type_ma = MultiArray.new([1, 2, 3], ["a", "b", "c"])
	mixed_type_ma.sort
	# ArgumentError: comparison of Fixnum with String failed

The methods subject to this restriction are max, min, sort, and sort_by. Since you probably don't have complete control over the types of the data stored in your data structure, the best strategy is probably to just let a method fail if the data is incompatible. This is what Array does:

	[1, 2, 3, "a", "b", "c"].sort
	# ArgumentError: comparison of Fixnum with String failed

One more example: in this one, I'll make Module itself include Enumerable. My each implementation will iterate over the instance methods defined by a class or module. This makes it easy to find methods of a class that meet certain criteria.

	class Module
	  include Enumerable
	  def each
	    instance_methods.each { |x| yield x }
	  end
	end

	# Find all instance methods of String that modify the string in place.
	String.find_all { |method_name| method_name[-1] == ?! }
	# => ["sub!", "upcase!", "delete!", "lstrip!", "succ!", "gsub!",
	# => "squeeze!", "downcase!", "rstrip!", "slice!", "chop!", "capitalize!",
	# => "tr!", "chomp!", "next!", "swapcase!", "reverse!", "tr_s!", "strip!"]

	# Find all instance methods of Fixnum that take 2 arguments.
	sample = 0
	sample.class.find_all { |method_name| sample.method(method_name).arity == 2 }
	# => ["instance_variable_set", "between?"]

See Also

  • Many of the recipes in Chapter 4 actually cover methods of Enumerable; see especially Recipe 4.12, "Building Up a Hash Using Injection"

  • Recipe 9.1, "Simulating Multiple Inheritance with Mixins"



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows