Ruby Iteration Methods - Part 2

01 OCTOBER 2013 | @ahsan_s | Software | Software | 1400 | 2
Column © 2008 uhx72
 Column © 2008 uhx72

In our last post we talked about Ruby iteration methods. Through numerous examples we saw how they help us write less code. We also listed the more commonly used iteration methods, with examples, that most Ruby built-in collection classes support.

We've come a long way on the topic of iteration, and we have a bit more to go. What we didn't talk about in our last article, however, is how the Ruby collection classes uniformly support those iteration methods. We promised to talk about it in this post, along with how we can create our own collection classes that also support all those iteration methods.

Without much ado, let's get started!




The Enumerable Module

It may seem like magic (at least it did to me at first) how Ruby collection classes uniformly support the iteration methods.

Magician's hat & wand

As I learned more I found out that Ruby achieves the feat by including the Enumerable[1] module in its collection classes. The only must-have requirement is that the collection class provide a method each, which yields successive members of the collection. If the collection is to also support max, min or sort, then the collection must also implement a meaningful <=> operator (also known as the starship operator).

That's it! All the iteration methods we saw in the list in the last post, and more, are available to any collection class just by implementing the above two simple methods.



A Custom Collection Class with Iteration Built-in

Now that we know the secret sauce, let's create our own collection class that supports the Enumerable methods.


Let's first create a simple Employee class, after which we'll create a custom collection of Employees.

Employee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# A simple Employee class
class Employee
  attr_reader :first_name, :last_name, :title, :age

  def initialize(fname, lname, title, age)
    @first_name = fname
    @last_name = lname
    @title = title
    @age = age
  end

  # A string representation of the Employee object
  def to_s
    "#{first_name} #{last_name}, #{title}, #{age}"
  end
end


Now we're ready to create a collection class for Employees. The only required method that we need to implement is each, as in the current context max, min and sort don't make much sense (if they did, we'd also have to implement the <=> operator). We'll also need a mechanism to add Employee objects to the collection, and we'll do this by implementing the << operator.

Let's call this collection simply Employees:

Employees
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# The collection class for Employee objects
class Employees
  include Enumerable

  def initialize
    @employees = []
  end

  # Add Employee objects to the collection
  def <<(employee)
    @employees << employee
  end

  # Method mandated by the Enumerable module
  def each
    @employees.each { |e| yield(e) }
  end
end

Where in the each method we're iterating through the internal array @employees, yielding each element e (i.e., each employee object) to the block that'd be supplied when the Employees' each method is called.



With the Employee and Employees classes in place, let's create an instance of the Employees collection class and then add some Employee objects to it:

Data
1
2
3
4
5
6
7
8
9
10
employees = Employees.new

employees << Employee.new('Anita', 'Baker', 'President', 48)
employees << Employee.new('Frank', 'Gifford', 'Director', 58)
employees << Employee.new('Barbara', 'Eden', 'Secretary', 34)
employees << Employee.new('George', 'Clooney', 'Project Manager', 37)
employees << Employee.new('Emily', 'Davies', 'Programmer', 28)
employees << Employee.new('David', 'Faber', 'Programmer', 55)
employees << Employee.new('Cindy', 'Adams', 'Programmer', 33)
employees << Employee.new('Helen', 'Hamilton', 'Business Analyst', 42)




Now we have everything, including data, in place to try some of the iteration methods we learned in our last post.


each_with_index calls block with two arguments, the element and its index, for each element in collection:

each_with_index
1
2
3
4
5
6
7
8
9
10
11
12
# List the items in the collection
employees.each_with_index{ |e, i| puts "#{i+1}. #{e.to_s}" }

# Output:
# 1. Anita Baker, President, 48
# 2. Frank Gifford, Director, 58
# 3. Barbara Eden, Secretary, 34
# 4. George Clooney, Project Manager, 37
# 5. Emily Davies, Programmer, 28
# 6. David Faber, Programmer, 55
# 7. Cindy Adams, Programmer, 33
# 8. Helen Hamilton, Business Analyst, 42


select returns an array for all elements of collection for which the given block returns a true value:

select
1
2
3
4
5
6
7
8
9
10
11
12
13
# Step-1: Select Programmers from the collection
programmers = employees.select{ |e| e.title == 'Programmer' }

# Step-2: Let's show the results
programmers.each{ |p| puts p.to_s }

# or simply (in one step)
employees.select{|e| e.title == 'Programmer'}.each{|p| puts p.to_s}

# Output:
# Emily Davies, Programmer, 28
# David Faber, Programmer, 55
# Cindy Adams, Programmer, 33


sort_by sorts the collection using a set of keys generated by mapping the values in the collection through the given block. I know it's a little mouthful, but an example will make it clear:

sort_by{ block }
1
2
3
4
5
# Sort by age
result = employees.sort_by{ |e| e.age }

# Show the results
result.each{ |e| puts e.to_s }

The return value of the block should be comparable using <=> operator. 'age' (integer), in this case, certainly is. So is String, as we'll use below.

Let's do some more sort_by examples using lambdas instead of blocks:

sort_by(&lambda)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# Let's create some lambdas to be used with sort_by
age = lambda{ |e| e.age }
first_name = lambda{ |e| e.first_name }
last_name = lambda{ |e| e.last_name }


# Step-1: Sort employees by age
result = employees.sort_by &age
# Step-2: Show the results
result.each{ |e| puts e.to_s }
# The above 2 steps can be combined in to 1 as follows


# SORT BY AGE:
employees.sort_by(&age).each{ |e| puts e.to_s }

# Output:
# Emily Davies, Programmer, 28
# Cindy Adams, Programmer, 33
# Barbara Eden, Secretary, 34
# George Clooney, Project Manager, 37
# Helen Hamilton, Business Analyst, 42
# Anita Baker, President, 48
# David Faber, Programmer, 55
# Frank Gifford, Director, 58


# SORT BY FIRST NAME
employees.sort_by(&first_name).each{|e| puts e.to_s}

# Output:
# Anita Baker, President, 48
# Barbara Eden, Secretary, 34
# Cindy Adams, Programmer, 33
# David Faber, Programmer, 55
# Emily Davies, Programmer, 28
# Frank Gifford, Director, 58
# George Clooney, Project Manager, 37
# Helen Hamilton, Business Analyst, 42


# SORT BY LAST NAME
employees.sort_by(&last_name).each{|e| puts e.to_s}

# Output:
# Cindy Adams, Programmer, 33
# Anita Baker, President, 48
# George Clooney, Project Manager, 37
# Emily Davies, Programmer, 28
# Barbara Eden, Secretary, 34
# David Faber, Programmer, 55
# Frank Gifford, Director, 58
# Helen Hamilton, Business Analyst, 42



It is interesting to see how much functionality we acquire just by implementing one method

Did you notice the use of '&' preceding the argument of sort_by? This is because the method sort_by expects a block as an argument, but we're passing it lambdas (age, first_name and last_name) instead; the ampersand (&) converts the lambda to a block for sort_by.

We could also use procs instead of lambdas in the above sort_by examples. I prefer lambdas here as it involves less typing. (The stabby lambda syntax would save even more keystrokes).





Spiraling Down © 2010 Barbi
 Spiraling Down © 2010 Barbi

If you had trouble following the above examples, you may need to review the contents of the last post and come back to this again.

Feel free to try other iteration methods from the last post on employees collection above.


It is really interesting to see how much functionality we acquire by implementing one method (each). Just the addition of the method, along with the inclusion of Enumerable module, has made our Employees collection class incredibly powerful in that it now almost magically supports all of the iteration methods described in the last post, and more (except max, min and sort).

If you want to try max, min or sort, you'll need to implement the starship operator (<=>). Feel free to give it a try.

If you're interested in how a module like Enumerable can be developed, you may want to take a look at Building Enumerable & Enumerator in Ruby.




In Closing


If you've also read the last two posts on this blog --- Closures in Ruby: Blocks, Procs and Lambdas and Ruby Iteration Methods, by now you should be fairly comfortable using Ruby blocks, procs, lambdas and a whole host of iteration methods. Hopefully this will make Ruby programming a lot more fun.

For more details on the iteration methods (Ruby Enumerable module) be sure to check the reference[1] below.






References:

  1. Enumerable: Help and documentation for the Ruby programming language
  2. Building Enumerable & Enumerator in Ruby: https://practicingruby.com