Ruby Iteration Methods

15 SEPTEMBER 2013 | @ahsan_s | Software | Software | 2300 | 5
stairs © 2006 kakki****
 stairs © 2006 kakki****

If you are writing in Ruby and still using for and while loops for iteration or accumulation over collections, you are almost certainly doing it the old-fashioned way without a good reason! Instead, you should really be using Ruby's built-in iteration methods, particularly if you really want to bring out Ruby's real expressive power. You'll also be hard-pressed to find experienced rubyists still using for and while loops.

Even if you don't care so much about looks (of your Ruby code, that is), you should still avoid for and while loops in favor of iteration methods. Your fingers will thank you!


Let's see if we can shed some light on the topic.

The first example is taken from an article[1] by Martin Fowler (I also had the audacity to modify it, but only slightly!). Assume we have a collection of employees from which we want to form another collection of employees who are programmers:

1
2
3
4
5
6
7
# The long and ugly
programmers = []
for e in employees
  if e.programmer?
    programmers << e
  end
end

The above code snippet, written using a for loop can be written using the iteration method select as follows:

1
2
# The short and sexy
programmers = employees.select{|e| e.programmer?}

If you've already mastered Closures in Ruby: Blocks, Procs and Lambdas, the syntax above should be a piece of cake to you. (If the syntax seems foreign to you, now may be a good time to read the article before proceeding).

All we're doing here is passing a block {|e| e.programmer?} to the collection's iteration method select, which iteratively calls the block, passing each element (e) to the block. Internally, select method inserts to a new array elements for which the block returns true. The new array is returned in the end.

Let's look at another similar example in action. Here we're given a collection of numbers (an array or a range), we want to create an array that only contains the even numbers from the original collection.

1
2
3
4
5
6
7
8
# Find all even numbers from given collection of numbers
[1,2,3,4,5,6,7,8,9].select{ |x| x%2 == 0 }

# or simply
(1..9).select{ |x| x.even? }

# Output:
 => [2, 4, 6, 8]

What if we want an array composed of elements of the original collection that are divisible by 3?

1
2
3
4
5
# Find all elements divisible by 3
(1..9).select{ |x| x%3 == 0 }

# Output:
 => [3, 6, 9]

How about array that excludes elements divisible by 3? If you thought of replacing == with != in the above example, you'd be right. But there's another way to do it, which uses the reject iteration method, that allows us to keep the block unchanged:

1
2
3
4
5
# Find all elements excluding those divisible by 3
(1..9).reject{ |x| x%3 == 0 }

# Output:
 => [1, 2, 4, 5, 7, 8] 


What if we have a problem where rather than selecting elements of the original collection based on some criteria (as we've been doing above), we need to convert the elements of a given collection according to some criteria and return the results in an array?

1
2
3
4
5
# Convert elements based on condition defined in block
%w[google amazon apple].map{ |x| "www.#{x}.com"}

# Output:
 => ["www.google.com", "www.amazon.com", "www.apple.com"]

Where we used map iteration method (as a one-to-one mapping exists between the original and the result collections). We could also use collect iteration method (as it collects the return values of the block), which is an alias for map, and we'd get the exact same result.


Let's look at a couple of more examples.

We want to know if all elements of a collection meet a given criteria:

1
2
3
4
5
6
7
8
9
# All element > 0
[1,2,3,4,5,6,7,8,9].all?{ |x| x > 0 }
# Output:
 => true

# All element > 0
[1,2,3,4,5,6,-7,8,9].all?{ |x| x > 0 }
# Output:
 => false

When we need to know if any element of a collection meet a given criteria:

1
2
3
4
5
6
7
8
9
# Any element < 0
[1,2,3,4,5,6,7,8,9].any?{ |x| x < 0 }
# Output:
 => false

# Any element < 0
[1,2,3,4,5,6,-7,8,9].any?{ |x| x < 0 }
# Output:
 => true




Okay, let's review what we've gathered so far, at a high level:

Spiral stairs © 2008 Armen
 Spiral stairs © 2008 Armen
  • iteration methods demonstrate the expressive power of Ruby that help us write less code
  • Ruby collection classes come with a set of pre-defined iteration methods
  • In order to use the iteration methods we need to familiarize ourselves with the methods, and what is expected of the blocks passed to those methods


Commonly Used Ruby iteration methods

A comprehensive list of Ruby iteration methods is beyond the scope of this article. However, below let's summarize[2], with easy to follow examples, some of the more commonly used iteration methods that most Ruby collection classes are equipped with.


Iteration method What it does Example
all? The method returns true if the block never returns false or nil.
# All element > 0
[1,2,3].all?{ |x| x > 0 }
# Output:
=> true

# All element > 0
[1,-2,3].all?{ |x| x > 0 }
# Output:
=> false
any? The method returns true if the block ever returns a value other than false or nil.
# Any element < 0
[1,2,3].any?{ |x| x < 0 }
# Output:
=> false

# Any element < 0
[1,-2,3].any?{ |x| x < 0 }
# Output:
=> true
collect Returns a new array with the results of running block once for every element.
(collect is an alias for map)
(1..4).collect { |i| i*i }
# Output:
=> [1, 4, 9, 16]
count Returns the number of items in the collection. If a block is given, it counts the number of elements yielding a true value.
[1,2,3,4].count
# Output:
=> 4

[1,2,3,4].count { |x| x.odd? }
# Output:
=> 2
detect Returns the first element in collection for which the block does not return false or nil.
(detect is the same as find).
(1..99).detect { |i|
  i%5 == 0 and i%7 == 0
}
# Output:
=> 35
drop Drops first n elements from collection, and returns rest of the elements in an array.
(1..6).drop(3)
# Output:
=> [4, 5, 6]
drop_while Drops elements up to, but not including, the first element for which the block returns nil or false and returns an array containing the remaining elements.
(1..6).drop_while { |i| i < 3 }
# Output:
=> [3, 4, 5, 6]
each The method passes each element of the collection to the block to process. (The method itself, however, returns the original collection).
result = []
(1..4).each { |i| result << i*i }
result
# Output:
=> [1, 4, 9, 16]
each_with_index Calls block with two arguments, the item and its index, for each item in collection.
hash = Hash.new
col = %w(cat dog gecko)
col.each_with_index { |item,index|
  hash[item] = index
}
hash
# Output:
=> {"cat"=>0, "dog"=>1, "gecko"=>2}
find find is the same as detect. (see example in detect above)
find_all Returns an array containing all elements of the collection for which the given block returns true.
(find_all is an alias for select).
[1,2,3,4].find_all { |e| e.even? }
# Output:
=> [2, 4]
find_index Returns the index for the first element for which the block does not return false or nil. If no object matches, returns nil.
(1..99).find_index { |i| i == 35 }
# Output:
=> 34

(1..10).find_index { |i| i == 35 }
# Output:
=> nil
first Returns the first n elements of the collection. (Doesn't take a block).
%w[google amazon apple].first(2)
# Output:
=> ["google", "amazon"] 

%w[google amazon apple].first(10)
# Output:
=> ["google", "amazon", "apple"]
group_by Groups the collection by result of the block. Returns a hash where the keys are the evaluated result from the block and the values are arrays of elements in the collection that correspond to the key.
(1..6).group_by { |i| i%3 }
# Output:
=> {1=>[1, 4], 2=>[2, 5], 0=>[3, 6]}
include? Returns true if any member of collection equals argument. Equality is tested using ==.
(include? is the same as member?).
%w[foo bar baz].include?("bar")
# Output:
=> true
inject Combines all elements by applying a binary operation, specified by a block or a symbol that names a method or operator.

If a block is specified, then for each element the block is passed an accumulator value (memo) and the element. If a symbol is specified instead, then each element in the collection will be passed to the named method of memo. In either case, the result becomes the new value for memo. At the end of the iteration, the final value of memo is the return value for the method.

If an initial value is not explicitly specified for memo, then the first element of collection is used as the initial value of memo.
(inject is the same as reduce).
# Sum some numbers
(5..10).inject(:+)
# Same using a block
(5..10).inject { |sum, n| sum + n }
# Output:
=> 45

# Multiply some numbers
(5..10).inject(1, :*)
# Same using a block
(5..10).inject(1){|prod, n| prod*n}
# Output:
=> 151200

# find the longest word
%w{cat sheep bear}.inject { |m, w|
  m.length > w.length ? m : w
}
# Output:
=> "sheep"
map map is the same as collect. (see example in collect above)
minmax Returns minimum and maximum values from a collection
a = %w(albatross dog horse)

# This form assumes all objects
# implement Comparable
a.minmax
# Output:
=> ["albatross", "horse"]

# Next form requires the block to
# use the comparison operator <=>
# known as the starship operator
a.minmax { |a, b|
  a.length <=> b.length
}
# Output:
=> ["dog", "albatross"]
minmax_by Returns a two element array containing the objects in the collection that correspond to the minimum and maximum values respectively from the given block.
a = %w(albatross dog horse)
a.minmax_by { |x| x.length }
# Output:
=> ["dog", "albatross"]
none? The method returns true if the block never returns true for all elements.
a = %w{ant bear cat}
a.none? { |word| word.length == 5 }
# Output:
=> true

a.none? { |word| word.length >= 4 }
# Output:
=> false
one? The method returns true if the block returns true exactly once.
a = %w{ant bear cat}
a.one? { |word| word.length == 4 }
# Output:
=> true

a.one? { |word| word.length > 4 }
# Output:
=> false

a.one? { |word| word.length < 4 }
# Output:
=> false
reject Returns an array for all elements of collection for which the given block returns false.
(reject is the opposite of select).
(1..5).reject { |num| num.even? }
# Output:
=> [1, 3, 5]
select Returns an array for all elements of collection for which the given block returns a true value.
(select is the same as find_all).
(1..5).select { |num| num.even? }
# Output:
=> [2, 4]
sort Returns an array containing the items in the collection sorted, either according to their own <=> method, or by using the results of the supplied block. The block should return -1, 0, or +1 depending on the comparison between a and b.
%w(bear cat ant).sort
# Output:
=> ["ant", "bear", "cat"]

(1..10).sort { |a, b| b <=> a }
# Output:
=> [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
sort_by Sorts the collection using a set of keys generated by mapping the values in the collection through the given block.
a = %w{apple pear fig}
a.sort_by { |word| word.length}
# Output:
=> ["fig", "pear", "apple"]
take Returns first n elements from the collection.
(Similar to first).
(1..5).take(3)
# Output:
=> [1, 2, 3]
take_while Passes elements to the block until the block returns nil or false, then stops iterating and returns an array of all prior elements.
(1..5).take_while { |i| i < 3 }
# Output:
=> [1, 2]


I don't know about you, but I stumbled upon group_by and inject when I first encountered them! I had to read them several times before I finally understood.


Another item that I probably should have included above is times. It is often overlooked as an iteration method, as it is different from the examples we've been seeing so far due to the fact that it iterates over collections that don't actually exist[3]! An example will make it clear:

1
2
3
4
5
6
3.times { |x| puts "The number is #{x}" }

# Output:
# The number is 0
# The number is 1
# The number is 2

It is as if we've just executed:

(0...3).each { |x| puts "The number is #{x}" }

The main purpose of times, however, is to repeat something a given number of times, as follows:

1
2
3
4
3.times { print "Ho! " }

# Output:
# Ho! Ho! Ho!




Sneak Peek


But how do Ruby collections uniformly support these iteration methods, and how can we create our own collections that support iteration methods?

That is the topic of the next post (October 1, 2013).




In Closing


stairs © 2009 Tulasi Nandan Parashar
 stairs © 2009 Tulasi Nandan Parashar

If you were not familiar with Ruby iteration methods, hopefully this post has introduced you to some of the expressive power and elegance of Ruby, and will encourage you to use iteration methods in your programs next time you're at your keyboard.

Remember to think long and hard when you're tempted to use for and while loops again. Chances are you'll come away with writing a lot less code, which will not only look elegant, but will also help keep carpal tunnel syndrome at bay!






References:

  1. CollectionLambda by Martin Fowler, 2005
  2. Enumerable: Help and documentation for the Ruby programming language
  3. Eloquent Ruby, Russ Olsen, Addison-Wesley Professional, 2011 (page 211)