3. Program Flow

Overview

By the end of the chapter, you will be able to utilize Boolean operators in Ruby programs; create and implement conditional expressions in Ruby; utilize ternary operators and ranges for programs; implement iterators on arrays and hashes and create programmatic loops with while, for, and until. The chapter aims to introduce us to different methods and entities involved in the Ruby program workflow.

Introduction

In the previous chapter, we studied the concept of arrays and hashes in Ruby. We also looked at different methods applied to arrays and hashes. In this chapter, we will be looking at how programs in Ruby are designed and used in applications.

Useful software programs are not simply a linear set of instructions; they make decisions about what code to run at any given time based on a set of conditions or criteria. Different programming languages have different types of program flow options. Two of the most common types of program flow options are conditionals and loops, which we will cover in this chapter.

Conditionals, also known as branches, are like a fork in the road. Do you turn left, or do you turn right? In the case of programming, for instance, you can decide what to do if a variable equals a value or is less than or greater than a certain value. Software simplifies the problem and makes the decision simply about truthiness: whether a condition is satisfied or not. A common example is evaluating a user's password and logging them in if the user-supplied value matches the value in the database or showing an error message if it does not.

Loops are another way to control program flow. A section of code will be repeated in a loop until a condition is met. In programming, you often want to loop over a collection of data such as an array and process the values in that collection. In some cases, the condition may never be met as the program may be configured to run forever. A web server, for instance, loops forever, waiting for new requests to come in to serve content.

In all types of program flow decisions, the fundamental unit is the Boolean. The Boolean represents truthiness or true or false values. We use the word "truthiness" to express that some values may not be an exact true or false Boolean but will be considered by the language as behaving as "true" or "false".

Since program flow is decided by truthiness, which is represented by Booleans, we need to learn how Booleans operate. We can do this by first learning about Boolean operators; there are three: AND, OR, and NOT. This is how we will begin our chapter.

Boolean Operators

As we know, Booleans tell us whether a value or condition is true and false. We can work with Booleans using the following three operators:

  • AND
  • OR
  • NOT

The AND Operator

In Ruby, the AND operator is represented by a double ampersand, &&. It represents what the truthiness is across two values if both are true. Consider the following sample code snippet:

var1 = true

var2 = true

var1 && var2

var1 = false

var2 = true

var1 && var2

var1 = false

var2 = false

var1 && var2

The output should look like this:

Figure 3.1: Output for the Boolean AND operator

Figure 3.1: Output for the Boolean AND operator

In the preceding example, the var1 and var2 variables depict true and false Boolean states in different combinations and the && operator gives results as per the combination.

The OR Operator

In Ruby, the OR operator is represented by a double pipe, ||. It represents what the truthiness is across two values if one is true. Consider the following sample code snippet:

var1 = true

var2 = true

var1 || var2

var1 = false

var2 = true

var1 || var2

var1 = false

var2 = false

var1 || var2

The output should look like this:

Figure 3.2: Output for the Boolean OR operator

Figure 3.2: Output for the Boolean OR operator

As depicted in the previous example, the var1 and var2 variables depict true and false Boolean states in different combinations and the || operator gives results as per the combination.

The NOT Operator

In Ruby, the NOT or negation operator is represented by a bang or exclamation point (!). It represents the opposite of a value. If a value is true, the negation of it is false, and if a value is false, the negation of it is true:

var1 = true

!var1

var1 = false

!var1

The output should look like this:

Figure 3.3: Output for the Boolean NOT operator

Figure 3.3: Output for the Boolean NOT operator

Truth Tables

The possibilities of these operators are best described with a truth table. Here is a sample truth table for the AND and OR operators:

Figure 3.4: Truth table for the AND and OR operators

Figure 3.4: Truth table for the AND and OR operators

As you can see from this table, there are some rules that can be inferred:

  • With AND, the result is true only if both are true.
  • Conversely, with AND, the result is false if either of the variables is false.
  • With OR, the result is true if either of the variables is true.
  • Conversely, with OR, the result is only false if both variables are false.

The following table is a truth table for the NOT operator:

Figure 3.5: Truth table for the NOT operator

Figure 3.5: Truth table for the NOT operator

As seen in the preceding table, the following statements can be inferred:

  • The result is true if the variable is false.
  • The result is false if the variable is true.

Truth tables are the base of Boolean algebra, which is beyond the scope of this book. However, let's cover two additional interesting properties that occur when we combine the negation operator with the AND and OR operators.

Consider the following statements:

  • !(x && y) is the same as saying !x || !y
  • !(x || y) is the same as saying !x && !y

Another way of looking at this is to say if you want to negate an operator used with two variables, then you distribute the negation to each variable and "flip" the operator. Flipping the operator means to switch between && and ||.

We can confirm this with a truth table, which is just a representation of two variables and all the possible combinations of true and false and the result of each operator:

Figure 3.6: Truth table of flipped operators

Figure 3.6: Truth table of flipped operators

Here is another truth table:

Figure 3.7: Truth table of flipped operators

Figure 3.7: Truth table of flipped operators

This is an example of one of the many laws of Boolean algebra.

Understanding Booleans and truthiness is a foundational concept that will lead us through to the use of all other aspects of program flow in Ruby.

Truthiness

As we mentioned, in Ruby, you can have Booleans that are direct representations of true and false. However, other values, such as strings and numbers, can be evaluated as to whether they should be true or false.

Take the number 0, for instance. Is the number 0 a true or false value? In other words, if we had an if condition statement that evaluated 0, Ruby should choose the true branch or the false branch. This is what we mean by "truthy." If a variable is "truthy," it means it doesn't contain a direct true value but will be treated as true for the purposes of program flow or Boolean evaluation. Different languages handle truthiness in different ways. Here is the truthiness table for Ruby:

Figure 3.8: Truthiness table for Ruby

Figure 3.8: Truthiness table for Ruby

As you can see, 0 evaluates to truthy In fact, in Ruby, almost everything evaluates to true except for nil and false. We can see this in an IRB console using a special trick: the bang, (!), and the double bang, (!!).

! is the negation operator in Ruby and therefore gives us the Boolean opposite of whatever value we are evaluating. When we apply the double bang operator, !!, we will get the Boolean opposite of the variable, which if you are following, gives us the Boolean value of the variable.

Let's look at the following example:

def is_truthy(var)

  if var

    puts "The var #{var} is truthy!"

  else

    puts "The var #{var} is falsey"

  end

end

is_truthy(0) # "The var 0 is truthy

is_truthy(nil) # "The var is falsey"

is_truthy(false) # "The var false is falsey"

is_truthy("") # "The var  is truthy!"

is_truthy(5) # "The var 5 is truthy!"

is_truthy("hello") # "The var hello is truthy!"

is_truthy(!0) # "The var false is falsey"

is_truthy(!!0) # "The var true is truthy!"

is_truthy(!nil) # "The var true is truthy!"

is_truthy(!!nil) # "The var false is falsey"

The output should look like this:

Figure 3.9: Output for truthiness

Figure 3.9: Output for truthiness

Precedence

In Ruby, there are both && and and as well as the || and or syntaxes. They serve the same purpose in that they both do a logical comparison, however, there is a small and notable difference in how they are interpreted by Ruby. The difference lies in the precedence of operators. In other words, Ruby will process some operators sooner than other operators.

The following is a table on operator precedence in Ruby

Figure 3.10: Operator precedence

Figure 3.10: Operator precedence

We can see that the logical && and || operators have higher precedence than the AND and OR operators. In most cases, the difference in precedence is negligible. However, there are a couple of situations where it is important, particularly when doing variable assignment:

var1 = true AND 1 # var1 == true

var2 = true && 2 # var2 == 2

In the preceding example, var1 is true because the variable assignment has higher precedence than the"and operator. However, var2 is equal to 2 because the && operator is evaluated first and is then assigned to the variable. The astute reader will note that even though true && 2 is a logical comparison, the assigned value is 2. Ruby will return the actual value of logical comparison if it is true. The value it returns depends on the operator.

Consider the following example:

var1 = 5 || true # var1 == 5

var2 = true or 5 # var2 == true

var3 = false || nil # nil

In the case of var1, we see that even though we are using the || operator, which has high precedence, var1 is getting assigned 5. This is because when Ruby processes the || operator, it will stop processing once it has satisfied the || operator (that is, it has reached a true condition). In the case of var2, the assignment operation happens first (var2 is assigned to true) and then is or'd with 5 (which is also true). In the case of var3, the Ruby interpreter can't satisfy the or condition with false, so it proceeds and then hits nil, which is then returned to the assignment operation and gets assigned to var3.

The general rule of thumb is that it is safe to use and and or in a program flow (for example, if/else conditions), whereas you always use && and || when doing variable assignment. While the and and or syntax is more readable than && and ||, it avoids sinister bugs that come up due to oversights in the order of precedence. Therefore, it is recommended to always use && and ||.

Exercise 3.01: Implementing Boolean Operators

In this exercise, we will demonstrate how Boolean operators work by creating a method that evaluates each of the operators. We can then pass different values in to see how the Boolean operators work. The following steps will help you with the solution:

  1. Open a new session on IRB.
  2. We first create a method that runs through all the scenarios of Boolean operators using two arguments. As you already know, puts is used to print the result of the operation:

    def truthiness(x, y)

      puts "AND: ", x && y

      puts "OR: ", x || y

      puts "!AND: ", !(x && y)

      puts "!OR: ", !(x || Y)

    end

  3. Apply the operators to different values for x and y:

    truthiness(true, true)

    truthiness(true, false)

    truthiness(false, true

    truthiness(false, false)

    truthiness(0, true)

    truthiness(nil, false)

    Here's the expected output:

    Figure 3.11: Output for Boolean operators

Figure 3.11: Output for Boolean operators

Conditional Expressions

Now that we are well-versed in Booleans and truthiness, we can begin to dive into program flow. We'll begin by discussing branches, which are also known as conditionals.

There are several keywords that denote conditionals:

  • if
  • else
  • elsif
  • unless

In Ruby, conditionals are structured into blocks of code that begin with a conditional keyword and end with an end keyword.

Let's look at each of them, one by one.

The if Statement

The if conditional is used to evaluate the existing condition of a variable and display the result accordingly. Consider the following example:

good_weather = true

if good_weather

  puts "Go outside"

end

The output should look like this:

Figure 3.12: Output for the if statement

Figure 3.12: Output for the if statement

Here, we initialize a good_weather variable, which will represent whether the weather is good. If it's good weather, we will output a statement telling us to go outside.

Next, we will see how to branch our code to handle good and bad, which are the weather conditions.

The else Statement

An else condition will help provide an alternate result if the desired condition is not satisfied. Consider the following example:

good_weather = true

if good_weather

  puts "Go outside"

else

  puts "Stay inside"

end

The output should look like this:

Figure 3.13: Output for the else statement

Figure 3.13: Output for the else statement

In this example, if the weather is good, we will get an output statement: Go outside. If not, the statement will be Stay inside.

The elsif Statement

We can use the else keyword to represent an either/or conditional. If we have more than two choices, we can use the elsif keyword to match multiple conditions. Look at the following example:

bananas_are_fresh = true

oranges_are_fresh = true

if bananas_are_fresh

  puts "Make a smoothie"

elsif oranges_are_fresh

  puts "Make orange juice"

else

  puts "Go to the farmers market to get more fresh fruit"

end

In the preceding example, there are two variables, bananas_are_fresh and oranges_are_fresh, which are set to true. If the first condition is met, the output statement will be Make a smoothie. If the second condition is also met, the output will be Make orange juice. If both conditions are not met, the output will be Go to the farmers market to get more fresh fruit.

The output should look like this:

Figure 3.14: Output for the elsif statement

Figure 3.14: Output for the elsif statement

The unless Statement

If statements, like most other programming languages, are saying if the following evaluation is true, then execute the next block of code. Ruby has an additional keyword to express a conditional when something is false. That keyword is unless. Look at the following example:

its_raining = false

unless its_raining

  puts "Go outside"

end

The output should look like this:

Figure 3.15: Output for the unless statement

Figure 3.15: Output for the unless statement

As you might guess, the unless keyword is the same as saying if not. While Ruby has elsif for additional conditions within an if/else block, there is no corresponding keyword for unless. Therefore, unless can only be paired with else, as in the following example:

its_raining = false

unless its_raining

  puts "Go outside"

else

  puts "Stay inside"

end

When deciding which keyword to use, one of the considerations should be readability. Which form makes it more readable to you and/or for others later on?

Let's look at an example that combines conditional keywords with Boolean operators:

its_raining = false

its_cold = true

if its_raining && its_cold

  puts "Bring rain jacket"

elsif its_raining

  puts "Bring umbrella"

else

  puts "It's nice outside, just don't forget your wallet"

end

The preceding code clearly depicts the use of the && Boolean operator to combine two conditions and give out the result accordingly. Additionally, if one of the conditions is met, the output will be Bring umbrella. If both conditions are not met, the output would be It's nice outside, just don't forget your wallet.

The output should look like this:

Figure 3.16: Output for the if statement

Figure 3.16: Output for the if statement

Note

It's good practice to steer clear of excessively lengthy and nested if/else statements. This is called a "code smell" because it makes you wince when you try to understand it. It's a good time to consider whether there is a better way to organize the code.

Comparison

We've learned how to evaluate the truthiness of a statement and how to branch code based on that truthiness. The last foundational concept in conditional program flow is how to compare two variables. We need to be able to determine whether two variables are the same, are not the same, or are less than or greater than each other.

Let's look at the operators used for comparison in Ruby.

Comparison Operators

The following are the operators most commonly used for comparison in Ruby:

Figure 3.17: Comparison operators

Figure 3.17: Comparison operators

Let's now write a method that combines all of the concepts in this chapter. We will be comparing two variables, x and y, and putting them through different comparison conditions:

def compare(x, y)

  if x < y

    puts "#{x} < #{y}"

  elsif x <= y

    puts "#{x} <= #{y}"

  elsif x == y

    puts "#{x} == #{y}"

  elsif x >= y

    puts "#{x} >= #{y}"

  elsif x > y

    puts "#{x} > #{y}"

  end

end

While this code appears to represent all the cases of each operator, it's actually problematic. We won't possibly encounter the == case, because if we evaluate to true for <=, this will evaluate first and we won't encounter the == case. In fact, we won't even run into the >= case.

Hence, a better way to write a comparison method would be as follows:

def compare(x,y)

  if x==y

    puts "x and y are equal"

  elsif x < y

    puts "x is less than y"

  else

    puts "x is greater than y"

  end

end

The choice of whether to use > and >= or < and <= depends on the situation and the problem you are trying to solve. This is called a boundary condition.

Comparing Strings

It is convenient to use comparison operators with integers, but it can be difficult with other types of variables, such as strings. Each object type (or class) in Ruby will define how to compare itself to other objects of the same type. For instance, when it comes to comparing two strings, we can think of it as follows:

"this string" == "something else" # false

That is straightforward, but the following condition is unrealistic:

"apples" > "oranges"

Let's use our compare method from the previous example to see how lots of different inputs behave:

compare("apples", "oranges") # x is less than y

compare("a", "a") # x and y are equal

compare("a", "A") # x is greater than y

compare("a", "b") # x is less than y

We are seeing some surprising things here. Why is a greater than A but less than b? The reason is because of how Ruby implements that comparison operator on strings. Most people will not memorize the internal algorithms of Ruby, so this is a good opportunity to read the docs.

When you go to the online Ruby documentation at https://packt.live/35mif9C, search for the type of object (or class) that you are looking for. In this case, string. There will be several results, usually for different versions of Ruby. Find your version of Ruby and dive into the string class. You will notice that there isn't an entry for <, >, or ==. Instead, Ruby implements all comparison operators with a single method: <=>, which returns -1, 0, and 1 to indicate less than, equal to, or greater than. All the other comparison operators (which really are just methods) are based on this central comparison operator. When you click on <=>, you will get an explanation of how Ruby decides to compare strings.

Exercise 3.02: Creating a Method to Check Your Balance

In this exercise, we will write a method to determine whether you have enough money to purchase something. We will determine the logic necessary for our balancer checker method and then implement it using conditionals and comparison operators:

  1. Open a new session on IRB.
  2. First, we determine the method. If you have more than enough money, you can make a purchase. If you have exactly enough money, you can make a purchase. If you do not have enough money, you cannot make a purchase:

    def can_purchase?(amount_in_bank, cost_of_item)

      if amount_in_bank >= cost_of_item

        return true

      else

        return false

      end

    end

    Here, we use the >= operator because as long as we have an amount equal to or greater than the cost of the item, we are okay to purchase it.

  3. We can also create an alternative implementation using a different operator:

    def can_purchase?(amount_in_bank, cost_of_item)

      if amount_in_bank < cost_of_item

        return false

      else

        return true

      end

    end

    Note

    It's up to you as the programmer to decide which is easier to read. You will also notice that we named our method with a question mark. This is good practice with methods that return Boolean values.

  4. Let's see how we can call this method in a conditional:

    bank_balance = 100

    cost_of_item = 200

    if can_purchase?(bank_balance, cost_of_item)

      puts "You can purchase this item"

    else

      puts "Sorry, you don't have enough money to buy this item"

    end

    You can see how when you write code with clearly named variables and method names, the resulting code is quite readable, even to people who are new to Ruby.

    Here's the expected output:

    Figure 3.18: Output for balance checking

Figure 3.18: Output for balance checking

Thus, we have successfully created a method to initiate/decline a purchase after checking the balance amount of the bank account by using conditionals and comparison operators.

The case/when Statement

Often, when programming, you will need to branch code based on many conditions. Using lots of if/else statements can result in spaghetti code, which is not easy to read or understand.

Spaghetti code refers to a pejorative term where the code is unstructured and normally has many goto statements, making it difficult to interpret.

An alternative to lots of if/else statements is to use the case/when keywords. Here is an example:

def get_animal_sound(animal_type)

  case animal_type

  when :dog

    "woof"

  when :cat

    "meow"

  when :cow

    "moo"

  when :bird

    "tweet"

  else

    nil

  end

end

Here, we have a method that depends on the type of animal we pass into it and returns a string with the sound it makes. This works by doing a comparison of the animal_type variable against each value in the case/when statement, going from top to bottom.

Try calling the methods with the following values:

puts get_animal_sound(:dog) # woof

puts get_animal_sound(:bird) # tweet

puts get_animal_sound("dog") # nil

The last call did not return the correct sound because strings and symbols are unequal and so the execution will continue until it hits the final else condition in the block.

An alternative format in the case/when statement is to leave out the variable to the right of the case keyword. In this format, Ruby will execute the statement following an evaluation to true.

Consider the following example:

def get_animal_sound(animal_type)

  case

  when animal_type == :dog

    "woof"

  when animal_type == :cat

    "meow"

  when animal_type == :cow

    "moo"

  when animal_type == :bird

    "tweet"

       else

    nil

  end

end

In this case, the expanded format is less ideal than the first since we are repeating the animal_type variable for each condition. However, this format can come in handy when you don't know all the cases ahead of time or need to do more complex logic such as doing a greater than or less than comparison.

The === Operator

You might naturally wonder about the difference between == and ===. First, let's remember that the == operator is actually a method that each Ruby object type (class) defines for itself so it knows how to compare two objects of the same type. The === operator is an additional method that Ruby gives us to do a similar but slightly different equality comparison. The === operator is often called the "case equality" operator or the "identity" operator. It allows comparison based on the type of object passed into it. Another way to put it is that the === operator determines whether the input passed to it is part of the set of what it's being compared to.

Consider the following example:

def guess_type(input)

  case input

  when String

    puts "It's a string!"

  when Integer

    puts "It's a number!"

  when Array

    puts "It's an array"

  when Hash

    puts "It's a hash"

  else

    puts "Not sure what you passed: #{input}"

  end

end

guess_type(5)

guess_type("5")

guess_type([1,2,3])

guess_type({foo: :bar})

The output should look like this:

Figure 3.19: Output for input type determination

Figure 3.19: Output for input type determination

What's happening here is the case statement is calling the === operator on the input and determining whether the input is part of the set. In this case, it is determining whether the type of object passed in is a type that belongs to the class it's being compared to.

Here are some further examples of the === operator:

String === "mystring" # true

Integer === 5  # true

5 === Integer # false?

Here, you can see in the last statement that something strange is going on. Integer === 5 holds true but 5 === Integer is shown as false. The reason is, while 5 is a type of Integer, Integer is a class and is not a type of 5. This concept will become clearer as we cover classes and ranges in upcoming chapters.

The Ternary Operator

Ternary means composed of three parts. In Ruby (and also in other programming languages), there is a common programming idiom called the ternary operator, which allows a quick if/else conditional to be put on one line by breaking it into three parts:

user_input = 'password'

secret = 'password'

user_has_access = user_input == secret ? true : false

The three parts are the condition (user_input == secret), the statement if true (true), and the statement if false (false). The three parts are separated first by a ? and secondly by a : notation.

While Ruby doesn't always require syntax such as parentheses, the preceding statement may be a bit hard to read unless you are very familiar with how Ruby handles the order of operations. Here is a clearer way of writing the preceding code:

user_has_access = (user_input == secret) ? true : false

The ternary operator is great for quick one-liners. However, if lots of complex logic is starting to creep into any of the three parts, this may be considered a code smell and the code will need refactoring into simpler logic. You can either refactor it into a proper if/else statement or you can refactor the complex logic into separate variables that are then evaluated in the ternary condition.

Consider the following example:

user_input = 'secret'

password = 'secret'

def login_user

  puts "logging in user"

end

def show_access_denied

  puts "Password was incorrect, try again"

end

user_has_access = user_input == secret

user_has_access ? login_user : show_access_denied

By using clearly labeled method names and simplifying the ternary statement, we can clearly see that if the user has access, we will log them in; otherwise, we will ask them to re-input their password.

Exercise 3.03: Speed Determiner

Write a method that determines the speed that a self-driving car should adjust to, based on environmental conditions, as well as the traffic status and distance to a traffic light. The method should return the new speed. Perform the following steps:

  1. Open a new session on IRB.
  2. First, we write the logic in pseudocode:

    Green light: 

          Sunny, all conditions: speed_limit

          Rainy, distance >= 50ft: speed_limit

          Rainy, distance < 50ft: 90% speed_limit

    Yellow light:

          Sunny, distance >= 50ft: 80% speed_limit

          Sunny, distance < 50ft: 50% speed_limit

          Rainy, distance >= 50f: 80% speed_limit

          Rainy, distance < 50f: 25% speed limit

    Red light:

          Sunny, distance >= 50ft: 50% speed limit

          Sunny, distance <= 50ft: 0% speed limit

          Rainy, distance >= 50ft: 25% speed limit

          Rainy, distance <= 50ft: 0% speed limit

  3. Implement the logic in a method. Define the method as drive_decision and also the parameters we consider in the method.

    Note

    We are introducing raise here, which will fatally exit the program if the code is encountered. This is a basic way to make sure that if a parameter is not passed in correctly, we get notified about it.

    def drive_decision(traffic_signal, weather, distance_to_signal, speed_limit)

      if traffic_signal == :green

        if weather == :sunny

          speed_limit

        elsif distance_to_signal >= 50

          speed_limit

        else

          speed_limit * 0.9

        end

      elsif traffic_signal == :yellow

        if weather == :sunny && distance_to_signal >= 50

          speed_limit * 0.8

        elsif weather == :sunny && distance_to_signal < 50

          speed_limit * 0.5

        elsif weather == :rainy && distance_to_signal >= 50

          speed_limit * 0.8

        elsif weather == :rainy && distance_to_signal < 50

          speed_limit * 0.25

        else

          raise "Condition not handled"

        end

      else # red light

        if weather == :sunny && distance_to_signal >= 50

          speed_limit * 0.5

        elsif weather == :rainy && distance_to_signal >= 50

          speed_limit * 0.25

        else

          0 # all other conditions should stop the car

        end

      end

    end

    Note

    raise is a keyword that raises exceptions. Exceptions can be caught and handled – this topic is covered later in the book.

  4. Evaluate the method with different variables:

    drive_decision(:green, :sunny, 100, 50)

    drive_decision(:yellow, :rainy, 50, 25)

  5. Refactor the method using case/when and additional methods. First, we determine the method for drive_decision_when_green:

    def drive_decision_when_green(weather, distance_to_signal, speed_limit)

          case weather

          when :sunny

                speed_limit

          when :rainy

                if distance_to_signal >= 50

                      speed_limit

                else

                      speed_limit * 0.9

                end

          else

                raise "Not handled"

          end

    end

  6. Similarly, we define the method for drive_decision_when_yellow:

    def drive_decision_when_yellow(weather, distance_to_signal, speed_limit)

          case weather

          when :sunny

                if distance_to_signal >= 50

                      speed_limit * 0.8

                else

                      speed_limit * 0.5

                end

          when :rainy

                if distance_to_signal >= 50

                      speed_limit * 0.8

                else

                      speed_limit * 0.25

                end

          else

                raise "Not handled"

          end

    end

  7. We also define the method for drive_decision_when_red:

    def drive_decision_when_red(weather, distance_to_signal, speed_limit)

          if distance_to_signal >= 50

                case weather

                when :sunny

                      speed_limit * 0.5

                when :rainy

                      speed_limit * 0.25

                else

                      raise "Not handled"

                end

          else

                0

          end

    end

  8. Lastly, we define the drive_decision method depending on the three conditions defined before:

    def drive_decision(traffic_signal, weather, distance_to_signal, speed_limit)

          case traffic_signal

          when :green

                drive_decision_when_green(weather, distance_to_signal, speed_limit)

          when :yellow

    drive_decision_when_yellow(weather, distance_to_signal, speed_limit)

          else

                drive_decision_when_red(weather, distance_to_signal, speed_limit)

          end

    end

    Here's the expected output:

    Figure 3.20: Output for driving decisions

Figure 3.20: Output for driving decisions

We can see, in the preceding code, that we are repeating code in the case statement. We can refactor this code by creating a new method that checks for valid traffic, weather states and writing this as a one-liner at the top of a method definition like this:

def drive_decision(traffic_signal, weather, distance_to_signal, speed_limit)

  raise "Unhandled weather condition" unless valid_weather_condition?(weather)

  #...rest of the method implementation…

end

def valid_weather_condition?(weather)

  [:sunny, :rainy].include?(weather.to_sym)

end  

This line of code, which starts off a method implementation, is called a guard clause because it makes a quick decision about whether we should proceed with the method implementation. In other words, it guards the method implementation by making sure only valid parameters are passed in. Guard clauses are a great way to refactor complicated if/else conditionals by checking for these conditions at the top of a method implementation with a guard clause. Then, you can keep the rest of your method implementation clean of additional checks on the data.

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

    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

    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

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

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

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

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

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: "[email protected]", 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

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

Figure 3.29: Output for the each_with_index method

Now, let's call each_with_index on a hash:

parameters = {id: 1, email: "[email protected]", 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

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: "[email protected]", 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

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: "[email protected]", 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

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

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

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

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

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

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

    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

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

    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

    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

    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.

Summary

In this chapter, we learned about program flow. Program flow is a foundational concept in programming that allows programmers to change execution paths dynamically depending on any number of conditions. A core concept within program flow is understanding the Boolean data type along with its truthy operators, AND (&&) and OR (||).

We learned that Ruby supports different program flow options, such as if and unless, which are logical inverses of each other. The decision to use one or the other depends on the programmer, who should opt for readability and maintainability.

We also learned how to loop and were introduced to Ruby blocks, which are bundles of code that get executed along with each iteration of a loop. Bundling code into reusable chunks is also a foundational concept. Another way to bundle code is by using methods, which we've looked at only briefly so far. In the next chapter, we will go into greater depth about methods by learning how to define them, how to set different types of parameters, and how to return values from them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.237.79