In Progress
Unit 1, Lesson 1
In Progress

Step

Video transcript & code

So far on this show we have covered a few different ways to iterate over a sequence of incrementing or decrementing integers, without resorting to a traditional for-loop. We've used Ruby ranges, as well as the #upto and #downto methods. We saw how both of those approaches have their own limitations.

Today, we'll look at a generalization of this concept.

Let's jump back to some code we saw in the last episode, #253. It prints a few verses of the song "99 Bottles of Beer on the Wall".

99.downto(95) do |n|
  puts "#{n} bottles of beer on the wall, #{n} bottles of beer"
  puts "Take one down, pass it around, #{n-1} bottles of beer on the wall"
end

# >> 99 bottles of beer on the wall, 99 bottles of beer
# >> Take one down, pass it around, 98 bottles of beer on the wall
# >> 98 bottles of beer on the wall, 98 bottles of beer
# >> Take one down, pass it around, 97 bottles of beer on the wall
# >> 97 bottles of beer on the wall, 97 bottles of beer
# >> Take one down, pass it around, 96 bottles of beer on the wall
# >> 96 bottles of beer on the wall, 96 bottles of beer
# >> Take one down, pass it around, 95 bottles of beer on the wall
# >> 95 bottles of beer on the wall, 95 bottles of beer
# >> Take one down, pass it around, 94 bottles of beer on the wall

Now let's say we want to get to the end of the song faster, by only printing every other verse. We can't do this with the #downto method. But we can do it with the #step method. We give #step two optional keyword arguments: the number to end on, and the number to add on each iteration. In our case, we tell it to end on 94, and step by -2. The result is an abbreviated rendition of the song.

99.step(to: 95, by: -2) do |n|
  puts "#{n} bottles of beer on the wall, #{n} bottles of beer"
  puts "Take one down, pass it around, #{n-1} bottles of beer on the wall"
end

# >> 99 bottles of beer on the wall, 99 bottles of beer
# >> Take one down, pass it around, 98 bottles of beer on the wall
# >> 97 bottles of beer on the wall, 97 bottles of beer
# >> Take one down, pass it around, 96 bottles of beer on the wall
# >> 95 bottles of beer on the wall, 95 bottles of beer
# >> Take one down, pass it around, 94 bottles of beer on the wall

As I mentioned, both arguments are optional. If we omit the by keyword, we get an upwards count by one.

1.step(to: 5) do |n|
  puts n
end

# >> 1
# >> 2
# >> 3
# >> 4
# >> 5

If we omit the to keyword, we get an infinite progression. Of course, this means we need to have some condition for breaking out of the block unless we want it to loop infinitely. Here's an example of a loop that searches for an unused ID.

ids = [3, 2, 5, 4, 1, 7]
id = 1.step do |n|
  break n if !ids.include?(n)
end
puts "Free ID: #{id}"

# >> Free ID: 6

(If the idiom of using break to return a value from a block is unfamiliar to you, check out episode #71.)

Also unlike the #upto and #downto methods, #step is available on all Numeric types, not just integers. This means that we could increment floating point numbers if we wanted to.

0.0.step(to: 1.0, by: 0.05) do |n|
  puts n
end

# >> 0.0
# >> 0.05
# >> 0.1
# >> 0.15000000000000002
# >> 0.2
# >> 0.25
# >> 0.30000000000000004
# >> 0.35000000000000003
# >> 0.4
# >> 0.45
# >> 0.5
# >> 0.55
# >> 0.6000000000000001
# >> 0.65
# >> 0.7000000000000001
# >> 0.75
# >> 0.8
# >> 0.8500000000000001
# >> 0.9
# >> 0.9500000000000001
# >> 1.0

Finally, as you might imagine from some of the other iteration methods we've explored on this show, there is also a form of #step that returns an Enumerator. Want to know the sum of just the even numbers from 0 through 100? We can set up the iteration and then, instead of supplying a block directly to the #step message, we can chain on sends to other Enumerable methods. In this case, we use #reduce to calculate a sum.

0.step(to: 100, by: 2).reduce(:+) # => 2550

(We introduced this usage of #reduce in episode #149)

While it isn't quite as expressive as special case methods like #upto and #downto, #step enables us to iterate over numeric sequences in a very generalized and flexible way. Hopefully, knowing about #step will save you from having to write a specialized loop with counter variables at some point. Happy hacking!

Responses