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.
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.
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
- 1.
Prompt the user for the height in feet of a building and store the response in a variable named “height.”
- 2.
Prompt the user for the width of the building and store that in a variable named “width.”
- 3.
Prompt the user for the length of the building and store that in a variable named “length.”
- 4.
Print a message back to the user describing the cubic volume of the building by multiplying height, width, and length together.
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.
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.
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
An example of pattern matching
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
- 1.
Capture the user input in a variable named “request.”
- 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.
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.
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.
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
While and do/while loops with an always false condition
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.
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
Accumulator pattern refactored as a for loop
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.”
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.
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.
Observe what happens when adding more than three tokens to a command. How might you handle the behavior differently?
- 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.
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.