What is Iteration?
Iteration is the repetition of a process. We've covered one form of iteration in an earlier lesson, but iteration also describes the manipulation of elements inside an array or hash. Fundamentally, there are 2 key differences between a loop
and an iteration
:
1. A loop
will repeat an action a defined number of times, while an iteration
works with a collection of data by allowing you to operate on each individual element.
2. Loops
require a user-defined ending somewhere in the method or they will break your program with an infinite loop. Iterations
could have a user-defined end if desired, but will cease after the last element in the collection has been iterated over otherwise.
You will do this thousands of times as a developer, and array manipulation is one of the core skills you will need to grow as a programmer. Fortunately in Ruby, if you exclude the loop methods like times
& while
, there are only 2 primary iteration methods you must learn - Each
& Map
.
How Iterators Compare to Loops
So aside from the two differences stated above, maybe you don't see the benefit to iterators and want to continue using loops. The best way to demonstrate the benefits of iterators is a direct comparison demonstration. Spoiler alert: Iterators will save you a lot of typing in the long run.
The first one will use the while
loop:
nba_draft_top_10 = ["LaMelo Ball", "Onyeka Okongwu", "Anthony Edwards", "Killian Hayes", "James Wiseman", "Obi Toppin", "Tyrese Haliburton", "Aleksej Pokuseveski", "Cole Anthony", "Deni Avdija"]
i = 0
big_board = nba_draft_top_10.length
while i < big_board
puts "#{nba_draft_top_10[i]} is one of the top rated prospects on my board."
i+=1
end
#=>LaMelo Ball is one of the top rated prospects on my board.
#Onyeka Okongwu is one of the top rated prospects on my board.
#Anthony Edwards is one of the top rated prospects on my board.
#Killian Hayes is one of the top rated prospects on my board.
#James Wiseman is one of the top rated prospects on my board.
#Obi Toppin is one of the top rated prospects on my board.
#Tyrese Haliburton is one of the top rated prospects on my board.
#Aleksej Pokuseveski is one of the top rated prospects on my board.
#Cole Anthony is one of the top rated prospects on my board.
#Deni Avdija is one of the top rated prospects on my board.
Now lets see the same iteration using the each
iterator:
nba_draft_top_10 = ["LaMelo Ball", "Onyeka Okongwu", "Anthony Edwards", "Killian Hayes", "James Wiseman", "Obi Toppin", "Tyrese Haliburton", "Aleksej Pokuseveski", "Cole Anthony", "Deni Avdija"]
nba_draft_top_10.each do |prospect|
puts "#{prospect} is one of the top rated prospects on my board."
end
#=>LaMelo Ball is one of the top rated prospects on my board.
#Onyeka Okongwu is one of the top rated prospects on my board.
#Anthony Edwards is one of the top rated prospects on my board.
#Killian Hayes is one of the top rated prospects on my board.
#James Wiseman is one of the top rated prospects on my board.
#Obi Toppin is one of the top rated prospects on my board.
#Tyrese Haliburton is one of the top rated prospects on my board.
#Aleksej Pokuseveski is one of the top rated prospects on my board.
#Cole Anthony is one of the top rated prospects on my board.
#Deni Avdija is one of the top rated prospects on my board.
What took us 6 lines of code with a while
loop took half that with an each
iterator. There are a few keys to how this happens:
1. Because iterators understand array length
inherently, there is no need to set a variable to establish a numerical value to it (big_board).
2. Iterators don't need counter variables (i) or conditions (i < big_board) to keep track when to stop.
3. Because of #2, you also have no need to manually increment your counter (i+=1).
4. As a bonus, the block provided by the each
iterator provides us a more accessible and readable variable (prospect) than the while
loop did (nba_draft_top_10[i]).
Take note that syntactically each
and map
are similar and you could switch them in the above example. So now that we've demonstrated the power behind iterators, let us break down each
and map
some more.
Each and Each's Extended Family
Each
is an iterator designed to iterate through your array without mutating the original array. Let's open up an irb
session and see what that means:
First we define the array.
irb(main):001:0> nba_draft_top_10 = ["LaMelo Ball", "Onyeka Okongwu", "Anthony Edwards", "Killian Hayes", "James Wiseman", "Obi Toppin", "Tyrese Haliburton", "Aleksej Pokuseveski", "Cole Anthony", "Deni Avdija"]
=> ["LaMelo Ball", "Onyeka Okongwu", "Anthony Edwards", "Killian Hayes", "James Wiseman", "Obi Toppin", "Tyrese Haliburton", "Aleksej Pokuseveski", "Cole Anthony", "Deni Avdija"]
Note that the implicit return is the entire array.
Now let's build our each
method line by line.
irb(main):002:0> nba_draft_top_10.each do |prospect|
irb(main):003:1* puts "#{prospect} is going to be good value where he is picked."
irb(main):004:1> end
LaMelo Ball is going to be good value where he is picked.
Onyeka Okongwu is going to be good value where he is picked.
Anthony Edwards is going to be good value where he is picked.
Killian Hayes is going to be good value where he is picked.
James Wiseman is going to be good value where he is picked.
Obi Toppin is going to be good value where he is picked.
Tyrese Haliburton is going to be good value where he is picked.
Aleksej Pokuseveski is going to be good value where he is picked.
Cole Anthony is going to be good value where he is picked.
Deni Avdija is going to be good value where he is picked.
=> ["LaMelo Ball", "Onyeka Okongwu", "Anthony Edwards", "Killian Hayes", "James Wiseman", "Obi Toppin", "Tyrese Haliburton", "Aleksej Pokuseveski", "Cole Anthony", "Deni Avdija"]
Now notice how the iterator runs automatically after pressing 'enter'. But just as important, notice after the iterator finishes, it implicitly returns the original array. This is the purpose of each
, to perform an action with a collection of data without mutating the original source.
There are actually several variations of each
iterators like each_with_index
, each_char
, each_with_object
and more. I will touch on each_with_index
, but other each
methods you should consult the Ruby Docs.
each_with_index
will do the same thing as each
, but takes a second argument inside the ||
block that will give a visible numerical value to the index. So if we wanted to turn our big board into rankings instead of a general top 10, we could do this:
nba_draft_top_10 = ["LaMelo Ball", "Onyeka Okongwu", "Anthony Edwards", "Killian Hayes", "James Wiseman", "Obi Toppin", "Tyrese Haliburton", "Aleksej Pokuseveski", "Cole Anthony", "Deni Avdija"]
nba_draft_top_10.each_with_index do |prospect, index|
puts "I have #{prospect} rated ##{index+1} overall."
end
#=>I have LaMelo Ball rated #1 overall.
#I have Onyeka Okongwu rated #2 overall.
#I have Anthony Edwards rated #3 overall.
#I have Killian Hayes rated #4 overall.
#I have James Wiseman rated #5 overall.
#I have Obi Toppin rated #6 overall.
#I have Tyrese Haliburton rated #7 overall.
#I have Aleksej Pokuseveski rated #8 overall.
#I have Cole Anthony rated #9 overall.
#I have Deni Avdija rated #10 overall.
So in essence, the second argument in the block effectively substitutes for a counter variable & incrementer.
Also, James Wiseman is probably lower on my big board. I value Onyeka Okongwu's defensive versatility over Wiseman's more likely gaudy stats and think most others here will have a more well-rounded game.
Map & it's Perhaps Evil Twin Collect
So I know I mentioned there were 2 primary iterators, then snuck in that each
has an extended family, but now I'm also adding a potentially evil twin? Yup, but I am exaggerating on the evil part. Map
and collect
are different names for basically doing the same thing: iterating through a collection and mutating that collection. I singled out collect
as the evil twin for 2 reasons:
1. Map
is a more universal term for this action and is used in many programming languages.
2. Collect
has more letters in it.
So let's open up a new irb session and use the same function we used with each
:
irb(main):001:0> nba_draft_top_10 = ["LaMelo Ball", "Onyeka Okongwu", "Anthony Edwards", "Killian Hayes", "James Wiseman", "Obi Toppin", "Tyrese Haliburton", "Aleksej Pokuseveski", "Cole Anthony", "Deni Avdija"]
=> ["LaMelo Ball", "Onyeka Okongwu", "Anthony Edwards", "Killian Hayes", "James Wiseman", "Obi Toppin", "Tyrese Haliburton", "Aleksej Pokuseveski", "Cole Anthony", "Deni Avdija"]
irb(main):002:0> nba_draft_top_10.map do |prospect|
irb(main):003:1* puts "#{prospect} is going to be good value where he is picked."
irb(main):004:1> end
LaMelo Ball is going to be good value where he is picked.
Onyeka Okongwu is going to be good value where he is picked.
Anthony Edwards is going to be good value where he is picked.
Killian Hayes is going to be good value where he is picked.
James Wiseman is going to be good value where he is picked.
Obi Toppin is going to be good value where he is picked.
Tyrese Haliburton is going to be good value where he is picked.
Aleksej Pokuseveski is going to be good value where he is picked.
Cole Anthony is going to be good value where he is picked.
Deni Avdija is going to be good value where he is picked.
=> [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]
Check out the implicit return. See how we are now returning nil
where we used to return prospect names? Well, it returns nil
due to a combination of map
mutating the array and puts
is inherently nil
. If we want to see the changes reflected, we just have to remove the puts
.
irb(main):005:0> nba_draft_top_10.map do |prospect|
irb(main):006:1* "#{prospect} is going to be good value where he is picked."
irb(main):007:1> end
=> ["LaMelo Ball is going to be good value where he is picked.", "Onyeka Okongwu is going to be good value where he is picked.", "Anthony Edwards is going to be good value where he is picked.", "Killian Hayes is going to be good value where he is picked.", "James Wiseman is going to be good value where he is picked.", "Obi Toppin is going to be good value where he is picked.", "Tyrese Haliburton is going to be good value where he is picked.", "Aleksej Pokuseveski is going to be good value where he is picked.", "Cole Anthony is going to be good value where he is picked.", "Deni Avdija is going to be good value where he is picked."]
Now we see something really interesting. We no longer have the puts
command to display our function item by item like in previous examples. Instead, we definitively see the mutations we made to the array reflected in a new array produced by the map
iterator that contains all the returned values.
For good measure, here is that same iteration using collect
:
irb(main):008:0> nba_draft_top_10.collect do |prospect|
irb(main):009:1* "#{prospect} is going to be good value where he is picked."
irb(main):010:1> end
=> ["LaMelo Ball is going to be good value where he is picked.", "Onyeka Okongwu is going to be good value where he is picked.", "Anthony Edwards is going to be good value where he is picked.", "Killian Hayes is going to be good value where he is picked.", "James Wiseman is going to be good value where he is picked.", "Obi Toppin is going to be good value where he is picked.", "Tyrese Haliburton is going to be good value where he is picked.", "Aleksej Pokuseveski is going to be good value where he is picked.", "Cole Anthony is going to be good value where he is picked.", "Deni Avdija is going to be good value where he is picked."]
Zero discernable differences.
Mutable vs Immutable
Since mutation is the key difference with these iterators, it's probably best if we touch on what situations to use each. I'm hardly the best to explain, I did find a really good article here, though it covers JavaScript uses the principle will remain the same. Now for most of what you will do while you are starting out, functionally, there will be little difference between mutable and immutable iterations. However, when you start building more complex apps, this nuance becomes more important. This is an over-generalization to be sure, but here it is:
1. Use each
when you are iterating through your back-end data. This is the stuff that you wouldn't want to permanently manipulate as it could affect other parts of your app. When using Ruby on Rails, your Controllers are a good example of places you want to use each
.
2. Use map
to iterate and manipulate data you want the user to see (front-end). Map
is often less bulky code-wise to work with since it naturally preserves itself by returning a new array.
Ruby Iterations Lab
Let's go collect
some practice to help map
out each
of our skills in the Ruby Iterations Lab!
Top comments (0)