© Jason Lee Hodges 2019
J. L. HodgesSoftware Engineering from Scratchhttps://doi.org/10.1007/978-1-4842-5206-2_6

6. Control Flow

Jason Lee Hodges1 
(1)
Draper, UT, USA
 

The Salesforce Tower is the tallest building in San Francisco and the second tallest building west of the Mississippi River. Its grandeur was made possible by the over 18 hours’ worth of work and 49 million pounds of concrete it took to pour its foundation. Without such intense effort and substance, such a spectacular building would be vulnerable to earthquakes, sinking, and swaying that could ultimately cause the building to collapse.

Up to this point you have only been writing and evaluating expressions in the Scala REPL. Just like the foundation of the Salesforce Tower, this has been in an effort to establish the strongest base possible for your software engineering education. Without such effort, you might encounter gaps in your understanding when introduced to complex concepts. That being said, at this point we have established enough context that you should be ready to actually start writing simple programs.

In this chapter, you will be introduced to Scala scripts and how to execute them from the command line. That will be followed by conditional statements and pattern matching. From there, we will cover the concept of loops that will allow you to repeat code. You will likely find this extremely useful for task automation. Finally, you will be shown how to handle errors in your code to keep the program from crashing unnecessarily. All of this will be framed using an example of a model operating system.

Scripting

So far, you have only been executing code directly in the Scala REPL for the interpreter to read and evaluate. And while it’s easy enough to copy and paste or rewrite small snippets of code from the command line, as programs get larger and have more opportunities for errors, it becomes increasingly more tedious to encounter errors when the code is evaluated at runtime. Running code exclusively from the REPL also makes it difficult to store and execute programs at a later time. Scala scripts allow you to write your Scala code in any code or text editor that you want and execute them from the command line at a later time when you are ready. Using this scripting strategy, you can plan out a small program from start to finish before ever evaluating a line of code, writing each line as a set of instructions like a recipe that will produce a final result. This makes it easy to change your mind, delete a line of code, or refactor a step in the process. Also, once you have code that you can refer back to, you can share that code with your colleagues so they can execute it or contribute to it.

To get started, download or open your text editor of choice. This could be a simple Notepad on a Windows machine that takes in plain text, or it could be a more full-featured Integrated Development Environment or Code Editor. One of the main advantages of using an IDE is it will often highlight syntax for you to let you know when you’ve made a mistake before you’ve even executed the code. This leads to a better, more productive developer experience. There is a large range of features that comes with each IDE, and choosing the one that you feel most comfortable with might seem daunting. For a beginner, it is recommended to use a code editor that has a light feature set and a simple user interface to minimize the number of things you need to learn in order to get up and running quickly. Examples of these might be programs like VS Code, Sublime, or Atom text editors (all of which should have some form of Scala Plugin to assist with syntax highlighting of the Scala language). Other more professional and heavy featured options include IntelliJ IDEA and Eclipse. While the choice is ultimately up to you, I prefer VS Code for lightweight development as it has a very active community of plugins and support.

Once you’ve chosen a text editor, you can open it up and write a simple line of Scala code in a new document. For the sake of example, let’s use the expression 2 + 2. Save this document with the file name example_script.sc to your computer somewhere. The .sc represents the file extension necessary for your computer to interpret the file as a Scala script. Next, open up a terminal and navigate to the location where you saved your Scala script. VS Code has a native terminal that, when opened, will take you directly to the location of the current working directory which is really nice additional feature. From there, type in the command scala -nowarn example_script.sc. This will execute your script and ignore any potential warnings that the compiler might have about your code. You can execute any Scala script this same way by providing the script name or script file path that you want to run as an argument to the scala command. It may take a moment for your code to be compiled and executed (which will speed up the next time you run the script), after which… nothing will happen. Congratulations! You just wrote and executed your first Scala program. However, your terminal simply returned back to input mode waiting for another command. So what happened? Why didn’t we see the result of our expression?

Unlike the REPL, an executed Scala program will only print feedback to the console or terminal if the code you write tells it to. You might wonder why that is. Some programs might have a lot going on for any given command or user input. A program might request data from the Internet, reformat it, save a temporary file to your computer, split that file up into several other files, and store it to several database systems, for example. If each of those actions required some output to be printed to the console, the user might get overwhelmed by the response to their input command. It is also not very efficient to print to the console for every command as each print is an extra instruction that the computer has to process. By eliminating mandatory print commands, you have the ability to control the user experience and the efficiency of your program.

Up to this point, the Scala REPL has been handling the input, output, evaluation, and looping for you, so you wouldn’t necessarily have a choice as to whether or not each expression evaluation gets printed to the console. Outside of the REPL when working with a Scala script, you as the developer have the control as to whether or not the evaluation step of the REPL process needs to print anything back to the console for the user to see. The best way to demonstrate this concept is to build our own REPL process. Just like the windows command prompt or the Mac terminal or bash shell, we can create our own command-line tool that has a REPL process. Our example shell, like a command-line operating system, will take user input in the form of predefined commands, evaluate the input, print out only what we want the user to see, and then loop back to wait for more user input. First, let’s examine controlling what gets printed to the console.

There are two functions that can accomplish this task, print and println. The former prints any String method that you provide to its parentheses and the later prints any String in the same fashion but then adds a new line character to the end of the String so that the next terminal output will be printed on a new line. To demonstrate the difference in their functionality, Listing 6-1 shows both the example_script.sc and its executed output.
example_script.sc
print(2 + 2)
println("Same Line")
println("New Line")
Terminal Output
4Same Line
New Line
Listing 6-1

Scala printing functions

Notice that because the print functions only take strings as arguments, the result of the expression 2 + 2 is coerced into a string and printed next to the “Same Line” string since it did not add a new line character to the end of the resulting string “4”. Alternatively, because the “Same Line” string was passed to the println function, a new line character was printed to the console so that the “New Line” string would end up on a new line. Also, because the “New Line” string was passed to a println function, any output that follows will also be on a new line.

Note

A new line character is a ASCII standard character that denotes the end of one line and the start of the next line. It is not a visible character in console output, but if you were to write it yourself in code, it would look like . You might also see the return carriage character that returns the cursor to the beginning of the same line or the tab character which represents the amount of space the tab key would occupy if you pressed it on the keyboard. If you like, you can manually add the new line character to the strings you wish to print instead of using the println function. However, it is common practice to just use the println function.

Now that you’ve seen how to code the print portion of the REPL process, how do you take user input? The function you can use to accomplish obtaining user input is the readLine() function. The readLine function takes a string in its parentheses that it can use as the text prompt that is printed to the terminal to let the user know that it is awaiting input. Once the user types something in that line and hits enter, Scala will take in whatever they typed as a string and the readLine function will evaluate it to a literal value as an expression. From there, you can store it as a variable or pass it to the print functions to echo back to the user exactly what they typed. Consider Listing 6-2 which shows a simple “times three” program that takes in user input, multiplies it by 3, and returns the output to the user.
times_three.sc
println(readLine("Enter a number: ").toInt * 3)
Terminal Output
Enter a number: 3
9
Listing 6-2

Demonstration of the readLine functionality

Notice that, because the number that the user typed in is coerced by Scala into a String, this script explicitly converts the user input into an integer to ensure that the program behaves exactly how it is intended to run. If the user had entered a string value that cannot be converted to an integer, the program would have thrown an error rather than return an incorrect answer (which would have been a string concatenated three times, which for this particular expression would have resulted in the string “333”).

Exercise 6-1

See if you can write your own Scala script that reads in user input and prints out user output according to the following specifications:
  1. 1.

    Prompt the user for the height in feet of a building and store the response in a variable named “height.”

     
  2. 2.

    Prompt the user for the width of the building and store that in a variable named “width.”

     
  3. 3.

    Prompt the user for the length of the building and store that in a variable named “length.”

     
  4. 4.

    Print a message back to the user describing the cubic volume of the building by multiplying height, width, and length together.

     
It might be useful to remind yourself when looking at this times_three.sc script in the future why you had to type in the .toInt method and what the expression would evaluate to if you didn’t. And even if you are confident that you would remember why you did write it that way, it might still be useful to leave a note for other developers to understand your logic. The way to do that in a Scala script is to leave a “comment.” A code comment is a line or block of code that you can write in your scripts that is not executed by the computer; it is completely ignored. It’s only purpose is to document whatever the developer wants to write to remind themselves or other developers something in particular about the code. You often see comments left in scripts to describe particularly complicated implementation logic or reminders to come back and change something about the code in the future. The syntax for a single line of comments is the double slash operator //. If you need more than one line for your comment, you can either put a double slash at the beginning of each line or you can use a block comment operator which begins with the start block comment operator /* followed by your comment spanning several lines before ending with the close block comment operator */. Examples of these operators are illustrated in Listing 6-3 for reference.
times_three.sc
// Added the toInt method to convert input to Int
println(readLine("Enter a number: ").toInt * 3)
/* If the toInt method were not added the string representation of the user input would just be concatenated together three times */
Listing 6-3

Example of code comments

This example will execute exactly the same as it did before, but it now has documentation added to it for future reference making it easier to collaborate. It is, however, a best practice to ensure that you are not adding a comment for every line of code, especially if that code is reasonably well understood by most developers. It is usually only important to add comments to leave reminders about particularly complicated blocks of code that might be difficult to read or interpret or to leave a “to-do” item for yourself or others to implement at a later time.

Conditional Statements

Up to this point, both in the REPL and in Scala scripts, you have only seen simple examples of straight-line programs. Straight-line programs are programs that run through a set of linear instructions from start to finish evaluating each non-comment instruction exactly once. Knowing that, you might easily glean that your programs would have a difficult time doing anything reasonably complex as a straight-line program without writing a considerable amount of code. In order to create more complex programs that can perform dynamic logic evaluations, you must create what is called a branching program. A branching program uses conditional operators to evaluate optional branches in your program. Unlike a straight-line program where all code is executed once, conditional branches in your code are only evaluated if the conditional operator that precedes the branch evaluates to true. If it does not evaluate to true, the branch is skipped entirely. Alternatively, if the conditional operator evaluates to false, your program can execute a different branch. You can think of the conditional operator as a gatekeeper to the different branches in your code. In this way, your code can act similar to a “choose your own adventure” type story. Code branches that are preceded by a conditional operator are referred to as conditional statements or sometimes if/then/else statements.

To write a conditional statement, you first type the if keyword, followed by a set of parentheses. Within the parentheses, you pass your conditional expression that will evaluate to either true or false. After the parentheses, the conditional statement takes a pair of curly braces that denotes the code branch that you would like to be executed if the conditional operation returns true. Any code that is within the curly braces will be considered part of this branch, also known as the scope of the if statement or the “then” statement. Some languages even use the keyword then to denote the scope. All code within the “then” branch should be indented to allow future developers to easily read the scope of the if statement without needing to first find where the first and last curly brace are. This is not enforced by the Scala language, but it is considered a best practice in order to ensure that collaborating developers are writing clean, readable, and easily modifiable code. If the conditional expression of this statement does not evaluate to true, the code branch will not execute at all. Scala will simply skip over anything contained within the if statement’s scope. Optionally, after the curly braces you can use the else keyword followed by another pair of curly braces to denote a separate branch that will execute if the condition is false. Both the if code branch and the else code branch can contain nested if statements as well to compound the branching logic. Listing 6-4 provides an example of this new branching syntax using a new Scala script that we will call nebula.sc. In our nebula.sc script, we will start to build out our mock operating system shell.
nebula.sc
var command = readLine("Provide Command: ")
if(command.contains("+")) {
      println(s"Addition command: ${command}")
}
else {
      println("Cannot evaluate command.")
}
if(command.contains("-")) {
      println(s"Subtraction command: ${command}")
}
else {
      if(command.contains("help")) {
            println("Help command")
      }
      else {
            println("Cannot evaluate command.")
      }
}
if(false) println("A") else if(true) println("B")
Terminal Output
Provide Command: 2 + 2
Addition command: 2 + 2
Cannot evaluate command.
B
Listing 6-4

Example of a conditional statement

Walking through this particular code, you can see that the first thing that we do is prompt the user to input a command. We store that input in the variable command. In this example, we are assuming that the user has typed 2 + 2 as their input, as shown in the terminal output section of the code listing. The next thing that we do is write our first conditional statement that evaluates whether the input string that the user provided contains the sub-string "+". Since 2 + 2 does contain the sub-string "+", the code branch contained within its curly braces executes and provides the output string "Addition command: 2 + 2". You’ll notice that because the if statement evaluated to true, the else code branch is ignored by Scala.

In the second conditional statement, we are checking to see if the user input contains the "-" sub-string. Because it does not, the condition evaluates to false and the branch that contains the code println(s"Subtraction command: ${command}") is ignored. Because the condition is false, Scala moves on to the else code branch which happens to contain a nested if statement. That statement checks to see if the user input contains the sub-string "help". Because the user input in our example does not contain that sub-string, the nested if statement’s code branch does not execute and Scala moves on to the nested else statement. That nested else statement prints out the string "Cannot evaluate command". Try running this same code with different user inputs to see how the output of the code changes similar to a “choose your own adventure” book.

In the last conditional statement, you’ll notice that it does not use any curly braces. This is considered inline shorthand for very simple expressions that can fit on a single line of your code. For reference, other languages have a concept called a ternary operator to provide this same logic of an inline conditional statement. For larger, more complicated expressions, it is a best practice to enclose your code branch in curly braces and provide proper indentation to show the nested nature of your code branch.

Hopefully you can see how conditional statements provide your programs the power to make dynamic decisions on the fly during the program’s runtime to allow a user’s input to customize the user experience. Listing 6-5 provides another example with an ice cream recommendation engine that has several if statements chained together to help solidify the concept in your mind.
var choice = readLine("What flavor of ice cream do you like? ")
if(choice.equalsIgnoreCase("chocolate")) {
      println("Rocky Road")
}
if(choice.equalsIgnoreCase("vanilla")) {
      println("French Vanilla")
}
if(choice.equalsIgnoreCase("fruity")) {
      println("Strawberry")
}
Listing 6-5

Examples of conditional statements

In this example, you can see an example of where the equalsIgnoreCase method is useful when comparing user input, since you don’t know what case they might have used. You can also see from this example that the else branch of each if statement is completely optional and is not used. You can chain together a series of if branches like this that only execute if the desired condition is met. You can see how this strategy might become a bit verbose over time if you need to evaluate a large set of conditions. For those scenarios, using the concept of pattern matching is a best practice.

Pattern Matching

Instead of chaining together if statements, a more concise syntax is called pattern matching where Scala looks to match a very particular pattern that you are evaluating for. If Scala finds a match, it will execute the code related to that pattern as denoted by the => operator. Some languages call this type of code a switch statement. A new version of the previous ice cream example refactored as a pattern matching statement is provided in Listing 6-6.
var choice = readLine("What flavor of ice cream do you like? ")
println(choice.toLowerCase match{
    case "chocolate" => "Rocky Road"
    case "vanilla" => "French Vanilla"
    case "fruity" => "Strawberry"
    case _ => "Unknown Flavor"
})
Listing 6-6

An example of pattern matching

Notice that the keyword to start the pattern matching expression is match followed by a pair of curly braces that captures the different cases or scenarios that you are trying to match on. The match keyword is used after the string or string variable that you are testing your match scenarios on. In this example, we are handling case sensitivity by converting the string variable to use all lowercase and then ensuring that each case condition is also lowercase. Each case condition within the match scope is indented as a best practice. When a condition is met, the code written after the arrow operator => is evaluated and returned as the result of the overall pattern matching expression. In this example, we are not evaluating any expressions just returning the literal value provided as the result of the match expression, which is then printed out to the console since the entire match expression is contained within a println function. You might have also noticed the underscore character in the last scenario. The underscore in Scala acts as a wildcard and in this particular expression it is the “catch all” condition in case none of the provided conditions are met. In some languages this is called the default case. Hopefully you can see how refactoring the code in this way is more clean and concise. Let’s refactor the code in our example operating system to use this new methodology in Listing 6-7.
println("Welcome to the Nebula Operating System (NOS)! Version 1.0.0")
var command = readLine("NOS> ")
println(command match {
    case c if c.contains("+") => s"Addition command: ${command}"
    case c if c.contains("-") => s"Subtraction command: ${command}"
    case c if c.equalsIgnoreCase("help") => "Help Command"
    case _ => s"${command} is not a known command."
})
Listing 6-7

Nebula script refactored for pattern matching

You’ll notice some additional syntax in this example. The pattern matching expression captures the input of the command variable as a new variable c that can then be checked with a condition using the if keyword. If that condition evaluates to true, then the pattern is considered a match and its corresponding expression will be returned. Otherwise, Scala will continue checking other patterns for matches. It is worthy to note that in a pattern matching expression, the first condition that matches the pattern will be returned by the expression, even if the input being checked for patterns might match multiple patterns in the expression. Thus, it is important to consider the order of the conditions when you write them (which is why the catch all/wildcard variable is last).

Exercise 6-2

Write your own pattern matching expression that returns the square footage that a piece of furniture would occupy in a building based on user input.
  1. 1.

    Capture the user input in a variable named “request.”

     
  2. 2.

    Create a case that will match if the request contains the sub-string “desk.” If the case matches, return the string “15 square feet.”

     
  3. 3.

    Create a case that will match if the request contains the sub-string “chair.” If the case matches, return the string “4 square feet.”

     
  4. 4.

    Create a default case to handle unknown requests.

     

At this point our Nebula shell program takes in three potential commands and returns output if those commands match a pattern. If none of those patterns match, the default case prints out feedback to the user that the command they typed was unknown. This operates fairly similarly to our description of the REPL process except that after the program receives the user input and prints out the corresponding output, the program ends. We need a way to loop back to the beginning of the script and await more user input to truly wrap up our REPL shell.

Loops

A loop is a method of control flow within your program that allows for the continual execution of a particular block of code until a defined condition is met. If that defined condition is never met, you may be trapped in an infinite loop that can cripple your computer, so you must always ensure that the condition that you define has the ability to be exited. In the event that you execute a script that gets stuck in an infinite loop, you can always hit Ctrl + C to kill your program and return back to the command prompt. There are three different types of loops that you will need to understand in software engineering: the while loop, the do/while loop, and the for loop.

While Loop

The while loop is defined by using the while keyword at any point in your code followed by a set of parentheses that contain the exiting condition. Following the parentheses, you provide the block of code that you want to be executed repeatedly. Just like conditional statements, you can provide the repeatable code inline without any curly braces or wrap the code block in curly braces and indent your code. Also like conditional statements, loops can be nested with other loops.

While loops are especially useful when you don’t know how many times you want the loop to execute, like in our Nebula OS example shell where we want to continue looping back in the REPL pattern until the user decides to shut down the program. Listing 6-8 illustrates how we would add a while loop to our program to continually loop back and ask for new user input. You’ll notice that there is a new command in the pattern matching scenarios that looks for the “shutdown” keyword.
println("Welcome to the Nebula Operating System (NOS)! Version 1.0.1")
while (true) {
    var command = readLine("NOS> ")
    command match {
        case c if c.contains("+") => println(s"Addition command: ${command}")
        case c if c.contains("-") => println(s"Subtraction command: ${command}")
        case c if c.equalsIgnoreCase("help") => println("Help Command")
        case c if c.equalsIgnoreCase("shutdown") => scala.util.control.Breaks.break
        case _ => println(s"${command} is not a known command.")
    }
}
Listing 6-8

Demonstration of a while loop

In this example, we’ve simply wrapped the entire code in the scope of the while loop using the curly braces. You’ll notice that the condition provided to the while loop is simply true. That means this code loop will execute infinitely until it is shut down. In order to ensure that the loop can be exited gracefully, the shutdown command uses a method from Scala’s utilities library called break. The break will exit out of the while loop, and the program will move on to the next line of code to execute. Because there is no additional code to execute after the while loop in this example, the program will terminate.

If you execute this scala script, you will notice that each time you enter in user input for the prompt NOS> your shell will evaluate the code, print out a response, and then loop back and prompt you again with another NOS> text string. When you are done testing each command in the pattern matching scenarios, you can type “shutdown” to execute the break. You’ll notice that Scala will print out a response of scala.util.control.BreakControl as a result of that break. To avoid that scenario, let’s initialize a variable outside of the scope of the loop so that we can change the exit condition of the loop. At the same time, we can look at an example of refactoring our code as a do/while loop instead of simply a while loop.

Do/While Loop

A do/while loop operates the exact same way as a while loop, except it uses the keyword do first, followed by curly braces surrounding the repeatable code block, and finally ending with the while keyword and its exit condition. The functional difference of the do/while loop is that the code block in the scope of the do statement is always executed at least once. Conversely, the normal while loop might have a condition that never evaluates to true and the code block is skipped entirely. If that happened in the event of a do/while loop, the code block would print exactly once instead of being skipped. An example of this scenario is demonstrated in Listing 6-9.
scala> while(false) println("Hello World")
scala> do println("Hello World") while(false)
Hello World
scala> do { var x = 1 } while (false)
scala> println(x)
<console>:12: error: not found: value x
Listing 6-9

While and do/while loops with an always false condition

Something to note about the scope of both conditional statements and loop statements is that variables defined inside their scope cannot be accessed outside of their scope, as demonstrated in the error in this example. Because of that, when we refactor our Nebula shell to use a do/while loop, we must initialize the command variable outside of the while loop’s scope in order to check its value in the exit condition. You’ll see an example of this and also an explicit message provided by the shutdown command in Listing 6-10.
println("Welcome to the Nebula Operating System (NOS)! Version 1.0.2")
var command = ""
do {
    command = readLine("NOS> ")
    command match {
        case c if c.contains("+") => println(s"Addition command: ${command}")
        case c if c.contains("-") => println(s"Subtraction command: ${command}")
        case c if c.equalsIgnoreCase("help") => println("Help Command")
        case c if c.equalsIgnoreCase("shutdown") => println("Shutting down...")
        case _ => println(s"${command} is not a known command.")
    }
}
while (!command.equalsIgnoreCase("shutdown"))
Listing 6-10

Refactoring the Nebula OS script to use a do/while loop

As you can see, the mutable variable command is assigned an empty string to start with outside of the do/while loop, followed by a repeating code block that reassigns the value of command each time it goes through the loop. Only after the reassignment and any executed command is the exit condition evaluated. That exit condition checks to see if the command variable has been assigned the string “shutdown”, and if it has, then the conditional statement evaluates to false and it exits the loop. If the command variable is assigned anything other than “shutdown”, the condition will evaluate to true and it will continue to loop. If you execute this version of the code and type in the “shutdown” command, your program will print “Shutting down...” to the console instead of scala.util.control.BreakControl and then the program will terminate.

This strategy of using a variable outside of the scope of the loop (known as a global variable) can be used in what is called the accumulator pattern. A pattern in software engineering is a repeatable strategy used to accomplish a particular task (you will learn more about that in the chapter on design patterns). In the accumulator pattern, the global variable is what is known as the accumulator. The accumulator is updated during each pass through the loop. In this way, the accumulator acts as the variable to be passed to the loop’s exit condition, and only when the accumulator has been updated to the appropriate condition does the loop exit. For example, if you want to repeat the same line of code a set number of times, you could do so using the accumulator pattern presented in Listing 6-11.
var i = 0
while(i < 3) {
      println(s"This is iteration #${i}")
      i += 1
}
Terminal Output
This is iteration #0
This is iteration #1
This is iteration #2
Listing 6-11

Demonstration of the accumulator pattern with a while loop

In this example the accumulator was initialized with a value of 0 and the while loop’s exit condition checks whether the accumulator is less than 3. By reassigning the accumulator to its previous value plus one (as denoted by the += operator) each time the loop executes, you will guarantee that the while condition will eventually exit and you can determine the set number of times you want the code to execute (which is three times in this example). If you had accidentally used the -= operator to continually decrement the accumulator by 1, the value of the accumulator would extend into the negatives and never be greater than 3, thereby trapping your code in an infinite loop. This accumulator pattern can be used in several scenarios where you need absolute control over the variables and conditions. However, it is so common to use it in the same way as the previous example that the for loop was created to streamline the syntax.

For Loop

The for loop can be called anywhere in your Scala code using the for keyword followed by a set of parentheses that take special syntax to replicate the accumulator pattern. First, you provide the name of an accumulator variable that you wish to initialize. Then you use the <- operator, followed by a group of data. Groups of data in Scala are considered iterable, and as such a for loop can iterate through each individual item within the group and assign its literal value to the accumulator variable. You can create the group of data inline with the for loop or ahead of time as a variable. Listing 6-12 shows a refactoring of our accumulator pattern using the for loop to demonstrate this methodology.
for(i <- Range(0,3)) println(s"This is iteration #${i}")
Terminal Output
This is iteration #0
This is iteration #1
This is iteration #2
Listing 6-12

Accumulator pattern refactored as a for loop

The way you would translate this syntax in English would be to say, “For an accumulator variable i in the range of 0 to 3 execute the following code.” You can see how the syntax is much less verbose in this example; however, this type of loop requires that you know exactly how many iterations need to occur. Just like while loops and conditional statements, the for loop executes a repeatable block of code following its parentheses. The code can be inline (as is demonstrated in this example) or it can be contained in a code block wrapped in curly braces. In this example, the way the for loop is told how many times to execute is not by using a conditional operator, but rather by iterating through a Range, which is a group of integer values that is generated using a start value and an end value. With each iteration, the value of the individual integer is assigned to the accumulator variable, and when each value has been assigned exactly one time, the for loop exits. Listing 6-13 demonstrates how you might use a for loop to iterate through other groups of data, either initialized inline or beforehand and stored as a variable.
var ints = Range(0,4)
var shows = Map("Friends" -> List("Ross", "Rachel", "Joey"), "Big Bang Theory" -> List("Leonard", "Sheldon", "Penny"))
for(i <- ints) println(s"This is iteration #${i}")
for(i <- List("football", "basketball", "baseball")) println(s"I like ${i}.")
for((show, characters) <- shows) {
      for(character <- characters) {
            println(s"${character} is in ${show}.")
      }
}
Terminal Output
This is iteration #0
This is iteration #1
This is iteration #2
This is iteration #3
I like football.
I like basketball.
I like baseball.
Ross is in Friends.
Rachel is in Friends.
Joey is in Friends.
Leonard is in Big Bang Theory.
Sheldon is in Big Bang Theory.
Penny is in Big Bang Theory.
Listing 6-13

For loop examples with different groups of data

Notice that in the last for loop the Map group of data is destructured into two accumulator variables. The first accumulator variable, show, represents the key of the map. The second accumulator value, characters, represents the value for each key. By assigning both the key and the value to a variable in the for loop, we can access their values in the scope of the for loop. You’ll also notice that the for loop scope contains a nested for loop that then uses that characters variable, which contains a list of characters for each show, as an iterator to assign a new accumulator variable the individual names of each character. This allows the nested for loop to print out a statement for each character in each show.

Exercise 6-3

Many language paradigms use the index value of items in a List to access the data within a for loop. See if you can use the length property of the list to initialize a Range that will iterate through each of the sports in the list in Listing 6-13 and print the same string using the index.

By now, you should be able to see the power of both conditional statements and loops. Using these two constructs, you could brute force your way to solving very complex problems and heavily repetitive problems with relatively little code. But what happens if unexpected user input finds its way into your code that doesn’t fit with the operations you are trying to perform? Often you will find that your script will crash when encountering these scenarios when really you would prefer to skip bad user input or handle the bad user input in some way similar to the default case in our operating system example. This can be accomplished using an error handling syntax known as the try/catch operators.

Exception Handling

An exception is an error event that occurs during the execution of a program that disrupts the instructions sent to the computer for processing. If not handled, an exception will halt the progress of the program. This is often referred to as the program “throwing an exception.”

The method used to handle an exception so that it does not halt your code is called a try/catch block. You can use the try keyword anywhere in your code followed by curly braces that contain the code you wish to try to execute. If an error occurs while executing code in the scope of the try code block, instead of crashing the program, the try block stops the execution of instructions and immediately skips to the catch code block which catches the error and allows you to do something with it. The catch keyword and a set of curly braces immediately follow the scope of the try block. Within the curly braces of the catch block, you can execute a pattern match to check for different exception types and you can handle each exception type differently. The most generic exception type is Throwable and it contains several sub-types. An example of a try/catch block is demonstrated in Listing 6-14.
try {
    throw new Exception()
}
catch {
    case _: IllegalArgumentException => println("Illegal argument provided.")
    case _: Throwable => println("An error occurred.")
}
Listing 6-14

Example of a try/catch block

This example exemplifies how to manually throw an exception within the try block as well as how to pattern match within the catch block. In the last pattern matching example, we stored the value of the string that we were matching into the variable c. In this example, we do not necessarily need to store the value of the error that we are matching on; we just need to ensure we are matching it by its data type. Because of this, we are using the underscore wildcard character to tell Scala to catch anything that is of the following type but don’t worry about giving its value a variable name. When you execute this example, the script will print out “An error occurred.” because it matches on the generic Throwable type (of which Exception is a sub-type). If you change the try block to say throw new IllegalArgumentException(), then the catch block will print out “Illegal argument provided.” instead since the pattern will match on that type. Again, be conscious of the order of your pattern matching as the first pattern that matches will be executed. It is thus a best practice to have the most specific exception to match on at the top and the most generic exception at the bottom.

If we look back on our Nebula Operating System, we’ll see that there is not really an opportunity to use this new try/catch logic anywhere in our code. Right now, anything that the user types in will either match one of the commands or default to the catch all case. That being said, we are not really doing anything special with any of the commands either. At this point, we can start to implement some additional logic for each command so that we can do some interesting things with our operating system and handle any exceptions to our logic using the try/catch functionality.

The first thing that we can do is actually try to process an addition command if the user is trying to add two numbers. You might recall from the topic of syntax that when parsing a set of lexeme for meaning, a compiler or interpreter will perform a process called lexical analysis. What that means is that it looks at each word in the sentence and tries to figure out what to do with it. In order to look at each word individually, it must first convert a string of characters into individual words or tokens. This is a process known as tokenization. There are several ways to tokenize a string command, but for the sake of simplicity, we are simply going to parse/tokenize each command by separating the string assuming a space character delimiter. If you remember from the section on string manipulation, we can do that using the split function. Listing 6-15 shows how we can accomplish this and wrap the code in a try/catch in case we get bad user input. The ellipsis is provided to show that the code has been abbreviated to only show the block that we are interested in changing.
nebula.sc
...
case c if c.contains("+") => {
    var tokens = c.split(" ")
    var plusIndex = tokens.indexOf("+")
    try {
        println(tokens(plusIndex-1).toDouble + tokens(plusIndex+1).toDouble)
    }
    catch {
        case _: Throwable => println("An error occurred trying to process an addition command.")
    }
}
...
Listing 6-15

Example of tokenization and a try/catch block

This code shows that in the case where the command contains a “+” character we can take the command and split it into a list of strings or tokens and store that list in our tokens variable. Then, we check what the index of the plus operator is within that array and store that in the plusIndex variable. Once we have those two things, we can attempt to add any two numbers that occur before and after the plus character in our list of tokens using their indexes, which are derived by taking the plusIndex and subtracting and adding 1 from it for the number before the plus and after the plus respectively. From there, we can print the result of the addition expression to the screen. In this example we are explicitly attempting to convert any number provided as a string by the user into a Double type. If this fails because the user input a string before or after the “+” character that cannot be converted to a Double, the catch block will catch our error and allow our while loop to keep listening for additional user input rather than crashing. If the user only provides a “+” character or only gives the shell a number on one side of the “+” character, the catch block will catch the corresponding error. Also, if the user does not provide spaces between the tokens in their command, an error will also be thrown as the tokenization process will fail to split the command into the appropriate list and the catch block will catch the error. All of these exceptions are handled the same way in our example by simply printing “An error occurred trying to process an addition command” to the screen. If you like, you can handle each of these exceptions in a different way if you allow the code to execute without a try/catch block and observe what error is thrown. Then you can pattern match on the specific exceptions that are thrown and tailor the message to the user to give them additional information as to what went wrong.

Exercise 6-4

  1. 1.

    Observe what happens when adding more than three tokens to a command. How might you handle the behavior differently?

     
  2. 2.

    Using the example provided for the addition command, implement the same tokenization logic for the subtraction command. Before adding the try/catch block, observe the program crashing when providing a bad input command. Then implement the try/catch logic to preserve the REPL session when bad input is provided.

     
  3. 3.

    Customize the help command to provide the user with specific instructions on how to use the addition and subtraction commands.

     

Summary

In this chapter, you learned how to execute a Scala script which allows you to store code for collaboration and execution at a later time. In these scripts you learned how to take in user input, print output, and leave code comments where necessary. You also learned several control flow processes for managing complex logic in your scripts. These control flow methods included conditional statements, pattern matching, loops, and exception handling. You also started building your own operating system shell using a Scala script. In the next chapter, we will continue to build out this operating system using an abstraction construct known as functions.

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

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