Ruby Arrays Part II - Iteration

bmweygant

Brandon Weygant

Posted on July 3, 2020

Ruby Arrays Part II - Iteration

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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. Mapis 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]
Enter fullscreen mode Exit fullscreen mode

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."]
Enter fullscreen mode Exit fullscreen mode

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."]
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
bmweygant
Brandon Weygant

Posted on July 3, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related