官术网_书友最值得收藏!

Loops

Loops are a type of program flow that repeat blocks of code. The ability to repeat a block of code allows us to do things such as processing collections of data. Typically, loops will run until a condition is satisfied. For instance, a loop may be run until it has run a certain number of times, or a loop may be run until it has processed all the items in a collection of data. Let's look at the following types of loops in Ruby:

  • while/do loops
  • until/do loops
  • do/while loops

The while/do Loop

Another foundational concept for program flow is being able to repeat sections of code until a condition is met. These are called loops and in Ruby there are many ways to create loops. The most basic structure of a loop contains two things:

  • The condition that will be evaluated to determine whether to repeat the code
  • The block of code to be repeated

Here is a simple block using the while keyword:

while true do

  puts Time.now

end

It should be fairly clear what this loop does. We can write this loop in English by saying, while true, output the current time:

  • while is the keyword that says evaluate while a condition is true.
  • do is the keyword that establishes the start of the potentially repeated block of code.
  • end is the keyword the declares the end of the repeated block of code.

In this case, true is obviously always evaluated to true and therefore this will loop forever. In other words, this is an infinite loop. Infinite loops are important concepts because they should either be intentional in the design of the program or otherwise avoided. An intentional infinite loop might be a program that runs forever, such as a web server.

Let's try another while loop that gets user input each time, which will determine whether we keep looping again or not.

Exercise 3.04: Developing a Mind Reader

In this exercise, we will write a method that picks a random number and asks us, the user, to try to guess it. The random number will be picked from a range of numbers supplied as a parameter to the method.

The following steps will help you to complete the exercise:

  1. Open a new session on IRB.
  2. Begin by creating the basic mind_reader method:

    def mind_reader

    end

  3. Define a method to pick a random number from a range and print it using puts:

    def mind_reader(range)

      magic_number = rand(range)

      puts "The magic number is #{magic_number}"

    end

  4. Now we get user input and check it against a random number. For that, we define guess to accept user input using gets.chomp and use conditionals to display the corresponding output statements:

    def mind_reader(range)

      magic_number = rand(range)

      puts "What's your guess?"

      guess = gets.chomp.to_i

      if guess == magic_number

        puts "That's right!"

      else

        puts "Sorry, that's not correct. The correct number is: #{magic_number}"

      end

    end

    The output should look as follows:

    Figure 3.21: Output for the incorrect number

  5. Write a loop to allow continued guessing until it's correct using the unless conditional. Once the guess matches the number, the output will show the corresponding output statement:

    def mind_reader

      magic_number = 7

      guess = nil

      while guess != magic_number do

        print "Nope! Try again! " unless guess.nil?

        puts "What's your guess?"

        guess = gets.chomp.to_i

      end

      puts "That's right!"

    end

    The output should look as follows:

    Figure 3.22: Output for the correct number

  6. Expand this loop to only give the user a certain number of guesses by using the comparison operator. That is how we limit the number of guesses:

    def mind_reader

      magic_number = 7

      max_guesses = 3

      attempts = 0

      guess = nil

      while guess != magic_number do

        print "Nope! Try again! " unless guess.nil?

        puts "What's your guess?"

        guess = gets.chomp.to_i

        break if attempts >= max_guesses

      end

      puts guess == magic_number ? "That's right!" : "You ran out of guesses, try again later!"

    end

    In this iteration, we establish the maximum number of guesses and keep track of how many attempts the user has made. However, this code could result in an infinite loop if the user never guesses the right number. We need to increment the number of guesses we are on. Let's add that in.

  7. Next, we increment the number of attempts:

    def mind_reader

      magic_number = 7

      max_guesses = 3

      attempts = 0

      guess = nil

      while guess != magic_number do

        print "Nope! Try again! " unless guess.nil?

        puts "What's your guess?"

        guess = gets.chomp.to_i

        break if attempts >= max_guesses

        attempts += 1

      end

      winner = "You've guessed it!"

      loser = "You ran out of guesses, try again later!"

      puts guess == magic_number ? winner : loser

    end

    Here's the output:

Figure 3.23: Output for incrementing the number of attempts at guessing

Thus, we have created a method where we cover all the different conditions of guessing a number.

The until/do Loop

while loops run while a condition is true. Conversely, Ruby has included the until/do construct, which runs code while a condition is not true or rather until the code becomes true. It is understandable that this converse logic can get confusing. It is similar to if and unless statements though. if statement blocks are run when a condition is true, whereas unless statement blocks are only run when a condition is false.

Consider the following example:

bank_balance = 0

cost_of_vacation = 1000

until bank_balance >= cost_of_vacation do

  bank_balance += 50

end

The loop clearly indicates that until bank_balance is greater than the defined cost_of_vacation variable, it will keep incrementing bank_balance.

The output should look like this:

Figure 3.24: Output for the until/do loop

The do/while Loop

The previous loops first evaluate the condition before running the block of code. Sometimes, you want to run the block of code at least once before evaluating whether you would like to repeat it. Let's refactor our mind_reader method to do this:

def mind_reader magic_number

  max_attempts = 3

  attempt = 0

  guess = nil

  loop do

    print "What's your guess?"

    guess = gets.chomp.to_i

    break if attempt >= max_attempts

    break if guess == magic_number

    attempt += 1

    puts "Nope! Try again"

  end

  puts guess == magic_number ? "That's right!" : "You ran out of guesses, try again later!"

end

Here, we are using the break keyword to exit the loop based on a condition. The break keyword is useful because it allows us to jump out of the loop at any time, not just after the full block has been executed. This allows us to output the Try again statement at the end but only if we really are trying again.

The output should look like this:

Figure 3.25: Output for the do/while loop using the break keyword

Here is another form of the do/while syntax:

keep_looping = :yes

begin

  print "Should we keep looping? "

  keep_looping = gets.chomp.downcase.to_sym

end while keep_looping == :yes

The output should look like this:

Figure 3.26: Output for the do/while loop

Here, we are establishing a block of code using the begin and end keywords. Begin and end blocks are useful not just for do/while loops but also for defining blocks of code for catching exceptions or errors in code. We will learn more about this later in the book.

Iterators and Enumerators

The loops we've seen so far are based on a condition that determines whether we should repeat the block of code or not. We can also decide to loop code blocks based on a set of data such as an array. This is known as iterating over a set. The following are common methods used to iterate over collections of data:

  • each
  • each_with_index
  • map/collect
  • select/reject

The each Method

each is one of the most common methods for iterating over sets of data in Ruby. For instance, if we have an array of three items, we can decide to loop over each item in the array:

[1,2,3].each do |i|

  puts "My item: #{i}"

end

The output should look like this:

Figure 3.27: Iteration output for the each method

Here, we have a three-item array and we call the each method on the array. This method returns an Enumerator, as follows:

[1,2,3].each.class #Enumerator

An enumerator is a Ruby class that allows iterations across a set of data. In order to iterate across a set of data, we also need a block of code and that block of code needs access to the singular item that is being worked on (or iterated over). This is where the do |i| syntax comes in. Do defines the block of code and |i| is the item in the array that the iterator is passing to the block of code. This is known as yielding a variable to the block.

The each method, as the name implies, simply iterates over each item in the array, passing it to the block. each is a core method that operates not just on arrays, but also any collection of data that includes hashes. Consider the following example:

parameters = {id: 1, email: "dany@example.com", first_name: "Dany", last_name: "Targaryen"}

parameters.each do |key, value|

  puts "#{key} has value: #{value}"

end

In the preceding code, we start by initializing a hash called parameters with some basic data of key-value pairs. We then call the each method on the parameters hash. Each key-value pair is yielded as separate arguments to the block within || characters. In the preceding example, the arguments are called key and value, but they can be have different names.

The output should look like this:

Figure 3.28: Output for the each method

We can see that when we iterate over a hash, the block takes two arguments, the key and the value for each key/value pair in the hash. It's up to each collection type to decide how it implements the each method and what arguments it will provide to the block. There is no limit to the number of arguments; it just depends on what is appropriate for the collection type. In most cases, there will either be one or two arguments in the block.

The each_with_index Method

The each_with_index method is very similar to the each method. However, as the name implies, this method not only provides the enumerated item in the loop but also the index of the item in the array. An example of when this is useful is when you may need to know the next or previous item in the array:

order_queue = ["bob", "mary", "suzie", "charles"]

order_queue.each_with_index do |person, index|

  puts "Processing order for #{person} at index: #{index}"

  if index < order_queue.length - 1

    puts "Next up is: #{order_queue[index+1]}"

  else

    puts "#{person} is last in the queue"

  end

end

The output should look like this:

Figure 3.29: Output for the each_with_index method

Now, let's call each_with_index on a hash:

parameters = {id: 1, email: "bob@example.com", first_name: "Bob"}

parameters.each_with_index do |key, value|

  puts "Key: #{key}, Value: #{value}"

end

The output should look like this:

Figure 3.30: Output with index of items in hash

Here, we see that the key is actually an array of the key/value pair, and the value is the index. This is interesting. What's happening here is that the hash implements the each method so that it returns a key and value to the iterator block. Now that we are calling each_with_index, the implementation is also going to send through the index to the block. You might be tempted to do the following to get each key, value, and index in separate variables:

parameters = {id: 1, email: "bob@example.com", first_name: "Bob"}

parameters.each_with_index do |key, value, index|

  puts "Key: #{key}, Value: #{value}, index: #{index}"

end

The output should look like this:

Figure 3.31: Output with separate variables

We can see this doesn't get us what we want. Let's try it differently:

parameters = {id: 1, email: "bob@example.com", first_name: "Bob"}

parameters.each_with_index do |(key, value), index|

  puts "Key: #{key}, Value: #{value}, index: #{index}"

end

The output should look like this:

Figure 3.32: Output with separate variables using parentheses

There we go. We've got each key, value, and index into separate variables by wrapping the key and value in parentheses in the block parameters. This is a special syntax in block parameters that allows arrays to be split into separate variables.

The map/collect Loop

Often, we want to iterate on a collection, process those items, and return a new collection of items. This is where the map and collect methods come in. The following is the implementation of the map method:

[1,2,3].map do |i|

  i + 5

end # [6,7,8]

The output should look like this:

Figure 3.33: Output for the map method

The map method, similar to the each method, iterates over each item in the array, but it collects the result of each iteration of the block in a new array. As you can see in the preceding example, the new array is the result of each item plus five.

Something to keep in mind here that is not obvious about Ruby is that the last line of the block is what's returned to the iterator. You might think that you should use the return keyword but that is not how Ruby is implemented. We will learn more about methods and the return keyword in the next chapter.

Also, an alternative syntax you can use to define the iteration loop is with curly braces, as follows:

[1,2,3].each {|i| puts i}

The output should look like this:

Figure 3.34: Output for the each method

In most cases in Ruby, you can replace do/end blocks with {} but there are some nuances, especially when working within IRB, to look out for.

Iterators are extremely handy for looping over any set or collection of data. For instance, here is how you can iterate over a hash that is really a collection of key/value pairs:

address = {country: "Spain",city: "Barcelona", post_code: "08001"}

address.each do |key, value|

  puts "#{key}: #{value}"

end

The output should look like this:

Figure 3.35: Output after using iterators

Here, we call the each method on the hash, and what is yielded to the block are two variables. Of course, the names of the variables passed to the block do not matter, but the position of them does. The Ruby hash is implemented such that it iterates over each key/value pair, and, for each pair, the key is yielded as the first variable and the value is yielded as the second variable.

There is no limit to what can be yielded to the block; it just depends on the collection being iterated over and how it is implemented. Let's take a look at how to implement our own iterator to learn more about how yielding works.

Imagine you have an array of product prices and you want to write a method that applies an arbitrary tax rate to them:

def with_sales_tax array

  array.map do |item|

    yield item.to_f

  end

end

prices = [5,25,"20",3.75,"5.25"]

sales_tax = 0.25

new_prices = with_sales_tax prices do |price|

  price * sales_tax

end

The output should look like this:

Figure 3.36: Output for the yield method

Here, we have implemented a method that loops over the array passed in as a variable to the method and performs a common operation, .to_f, in order to sanitize the input and make sure all the items are floating-point numbers. Then, the method yields to the block associated with the call to the method. The transformation (adding a sales tax percentage) is the last line of the block, which is then passed as the last line of the call to map inside the method, which collects the sanitized and transformed prices and passes them back as the result of the method.

Blocks and yielding are one of the trickiest parts of learning Ruby when you first get started. Let's explore another way of writing the same code:

def with_sales_tax(array, &block)

  array.map do |item|

    block.call(item.to_f)

  end

end

prices = [5,25,"20",3.75,"5.25"]

sales_tax = 0.25

new_prices = with_sales_tax prices do |price|

  price * sales_tax

end

The output should look like this:

Figure 3.37: Output for block as an argument

In this form, the block is explicitly passed as an argument to the method. An ampersand is used as a prefix to tell the Ruby interpreter that the argument is a proc object. A proc object is a block of transportable, runnable code and will be covered in Chapter 11, Introduction to Ruby on Rails I.

Exercise 3.05: Developing an Arbitrary Math Method

In this exercise, we will develop a method that accepts an array of numbers. It will yield two elements in the array to a block that performs a mathematical operation on them (addition, subtraction, division, and so on) and returns the result to be processed in the next iteration:

  1. Define the math method:

    def math(array)

    end

  2. Implement the method. In order to yield two items from an array, process the result and then take the result and apply it again to the next item in the array. The easiest way is to continue to shift items out of the array. We begin by shifting the first item out of the array:

    def math(array)

      first_item = array.shift

  3. Then, we continue to loop over the array while there are items in the array with the while loop:

      while(array.length > 0) do

  4. Then, we need to get the value of the second_item variable from the array:

        second_item = array.shift

  5. Pass the two items to the block by yielding them individually as arguments to yield:

        first_item = yield first_item, second_item

      end

      return first_item

    end

    The output should look like this:

    Figure 3.38: Output for the math method

    You'll notice that we capture the result of the block in the first_item variable. This is an important point. The return value of the block (even though you don't specify the return keyword) is what comes back from yield and you can capture it in a variable.

    In our exercise, we then return to the top of the while block and evaluate whether there are items in the array to continue processing. If so, we shift the next one along as our second argument. In this case, now, our first argument is actually the result of the previous yield call.

    While we've called this an arbitrary math function, really this implementation can be used for any processing of subsequent items in a set of data. This is actually how you could implement map/reduce, which is a programming paradigm for processing large datasets very quickly and is a common paradigm in functional programming languages.

  6. Process three variables at a time:

    def math(array)

      first_item = array.shift

      while(array.length > 1) do

        second_item = array.shift || 0

        third_item = array.shift || 0

        first_item = yield first_item, second_item, third_item

      end

      return first_item

    end

    The output should look like this:

Figure 3.39: Output for the math array

You'll notice that we modified a couple of lines to handle nil references:

second_item = array.shift || 0

third_item = array.shift || 0

Because we are processing three items at a time, this allows arrays with lengths that do not divide evenly into three to still be processed. This works great for addition, but will it work for all the other mathematical operations?

Also, a good follow-up exercise would be to implement this method and parameterize the number of items to process at a given time. As such, the usage would look like this:

math([3,4,5,6], 2) { |a,b| a + b }

math([3,4,5,6], 4) { |a,b,c,d| a + b + c + d }

Activity 3.01: Number-Guessing Game

In this activity, write a high/low guessing game. The computer will pick a random number and the player needs to guess what it is. If the player guesses incorrectly, the computer will respond with a higher or lower statement, indicating that the player should guess a higher or lower number, respectively. The program exits when the player guesses correctly.

The following steps will help you with the solution of the activity:

  1. Open a new session on IRB.
  2. Create a play_choice method that allows a user to choose to play or exit. It should define the variable used for the Yes condition and continue to play:

    Figure 3.40: Output for the yes input

    Similarly, if the input is No, display a thank you message, as shown in the following figure:

    Figure 3.41: Output for a no input

  3. Implement a single guess method. This method will employ various conditions for guessing a number. It will suggest that the player guesses lower/higher if their guess is incorrect. Also, it will print a message saying You guessed correctly! when the guess is correct, as shown in the following figure:

    Figure 3.42: Outputs displayed for the guessing attempts

  4. Put the whole program together with a play_game method, where the game is initiated using a while loop and the conditions are followed.

    Here's the expected output:

Figure 3.43: Output for the HiLow game

Note

The solution for the activity can be found on page 462.

主站蜘蛛池模板: 县级市| 客服| 杂多县| 二连浩特市| 定南县| 沂水县| 封开县| 福州市| 白城市| 高碑店市| 阜宁县| 灵台县| 鄂州市| 黑山县| 两当县| 许昌县| 大厂| 哈密市| 义马市| 娱乐| 三门峡市| 中西区| 闸北区| 通山县| 皋兰县| 铁岭县| 舞阳县| 宁陕县| 昌平区| 郓城县| 碌曲县| 吕梁市| 靖远县| 达拉特旗| 濉溪县| 嘉兴市| 青阳县| 临邑县| 虞城县| 祁门县| 彭泽县|