With regard to the subject of mechanical engineering, there are six simple machines that are considered the building blocks from which all more complex machines are composed. These include the lever, the wheel and axle, the pulley, the incline plane, the wedge, and the screw. It is fascinating to know that even the most complicated of our modern machines, like cars, robots, or even computers themselves, are made up of such simple concepts. But, just like in mechanical engineering, there are a finite amount of fundamental components in software engineering that compose all programs and programming languages. These include variables, expressions, conditionals, loops, functions, and classes – all of which have been covered in this book up to this point. Given that you now know all of the fundamental pieces of programming in Scala, you would be justified in feeling confident enough to build any program you wish with the knowledge you already have.
However, as your programs begin to increase in size and complexity, it becomes important to organize your code into separate files and folders for further abstraction and/or modularity and to help facilitate collaboration. In this chapter, you will expand your knowledge of the Scala object which you were briefly introduced to when learning about classes. You will also be introduced to a method of organizing objects into a larger encapsulation mechanism known as packages. Finally, you will learn how to reference members between packages and objects through the use of imports. That being said, cross file referencing is not possible in Scala without first compiling your code, so let’s first take a brief look at the Scala compiler.
Compiler
Up to this point, all the code that has been written in this book would be considered interpreted code. Interpreted code is code that can be written and rewritten several times and is only translated for the computer to understand at the time that it is run. Whether it is coded from the REPL or written iteratively from a Scala script file, interpreted code is seen by many as a really great paradigm for rapid application development, proof-of-concept prototyping, or smaller utility type programs that help with task automation processes.
As your programs become larger and more complex, the need to separate them into isolated files for organization and collaboration becomes increasingly apparent. In Scala, in order to separate a project into multiple files, you must first compile the code. Compiled code is translated into machine code at compile time rather than when the program is run. Compilation is an extra step that must take place before you can run your code. Compiled code is generally considered much faster than interpreted code because (among other things) it is translated ahead of time rather than at runtime. Additionally, when working on a large project with multiple files split out, only the files that are changed will need to be re-compiled, thus providing added productivity when prepping a code base to run. Conversely, all interpreted code is translated into machine code every time you run it. Examples of interpreted languages include Python, JavaScript, and Ruby, whereas examples of compiled languages include C, C++, C#, Rust, and Java. While there are varying implementations of all of these languages that can blur the lines between compiled and interpreted, Scala is one of the very few languages that comes with both an interpreted option and a compiled option out of the box, thus allowing you to easily learn both.
So how does compilation work for Scala? You might recall that when you installed Scala, you first had to ensure that you had a version of the Java SDK installed already. Scala, when compiled, is translated down to what is called Java Byte Code. Java Byte Code is code that has been compiled to be translated, not by an actual computer but by a virtual computer known as the Java Virtual Machine or JVM. That virtual machine is installed and running on any machine that you want to run your code on as a prerequisite. The reason Scala is compiled to Java is because so much work has been put into the JVM so that it will work on any computer. One of the biggest selling points for writing Java code in its infancy was that you could write code once and execute it on any machine because there was a JVM version for every type of computer out there. Conversely, other languages required you to have a very specific compiler to translate your code depending on what kind of machine you would be executing your code on and you would need to test and troubleshoot your code on each compiler for compatibility. You might see how that could get tedious.
By compiling Scala code down to Java code, not only are you able to leverage the work of the JVM, but you also get access to the entire Java community of code if you choose to collaborate with Java developers. Once compiled, Scala and Java code operate exactly the same so there is often really good interoperability between the two languages. When compiled, both Java and Scala code are transformed into files called .class files. From there, the .class files can be run by either Java or Scala. The command to compile a Scala file is scalac followed by the Scala file name (the “c” at the end of Scala standing for “compile”). This will create a .class file of the same file name in the same directory as the scala file that you are compiling (unless you are compiling a package – more on that later in the chapter). From there, you can execute the compiled code using the command scala followed by the name of the class file omitting the .class extension.
Note
You cannot compile a Scala script file unless your code is wrapped in either a class or an object. You were briefly introduced to the object type in Scala in the previous chapter when the topic of the companion object was covered. However, an object in Scala can also be a stand-alone structure that can be used to encapsulate code on its own.
Scala Object
A compiled “Hello World” program wrapped in an object
You’ll notice that, just like the companion object, in order to create a basic object in Scala, you use the object keyword followed by the name of the object. In order to make your code executable after compilation, you also add the keywords extends App. This allows Scala to know that when your code is executed using the scala examples command, all of the code within the body of the object should be called as if it were running a function. If you did not add the extends App keywords and you called the scala examples command, the JVM would not find an entry point to start running your code and it would throw an error. This will become more apparent once you’ve started separating your code into several files.
You may have also noticed that when you ran scalac examples.sc, it created a file called examples.class in the same directory as your examples.sc file. The examples.class file is your compiled code. By calling scala examples and omitting the .class extension, you are telling Scala implicitly to run an already compiled .class file (regardless of whether it was originally written in Scala or Java). The behavior of the directory that your files compile to is altered slightly, however, when you are compiling files that belong to a package.
Packages
Examples of referencing and compiling multiple files from the same package
You might notice that the extension for the files in this example is no longer a .sc extension but rather a .scala extension. Since these files are not going to be interpreted but rather always compiled, it is better to use the .scala file extension, which is equivalent to the Java .java extension and is meant for objects and classes, rather than the Scala script extension.
The two files in this example both start with a declaration that they belong to the package “examples.” This gives them access to each other’s members implicitly. Thus, when the messenger file wants to access a member of the messages object, it can do so as if the code were written in the same file. You’ll notice that only the messenger object uses the extends App keywords as that is the entry point for our now multi-file program. We haven’t declared anything to happen in the messages file, so its purpose is simply to exist as a reference for our main program, messenger, to make a call to one of its members. Once both files have been written, they are compiled at the same time using the command scalac messenger.scala messages.scala which creates a package directory that contains the compiled class files for both files. If you make a change to one of these files and not the other and need to re-compile, you only need to include the name of the changed file after your scalac command. To execute our program, we simply call the main entry point file from its package directory using the same dot notation that you would use to access the member of a class or object. The command scala examples.messenger looks in the package examples for the messenger object and executes it. This prints out the message “Hello World!” which is a string member that exists in the messages object.
Imports
Explicitly importing a stringWrapper object to the messenger app
Example of explicitly importing multiple methods from an object
You will also notice from the example in Listing 9-3 that the examples package now adds curly braces around the messenger object to denote the package scope is separate from the import. It’s worthy to note that just like the scope of a function or a conditional branch, packages can have nested scopes of sub-packages if you wish, denoted by nesting curly braces and additional package keywords.
If you still do not have the full understanding of why you would explicitly import objects rather than keeping everything in the same package, a great example would be to look at some of the functionality that can be imported from the Scala standard library. You’ve seen examples of these Scala standard library features in some of the example code thus far in the book because your Scala programs have implicit access to their methods as long as you reference the entire package namespace. However, your code can be dramatically abbreviated by importing the Scala Standard Library package you want to use at the top of your files instead of referencing the entire package name inline with your code.
Standard Library
An example of importing the math package and the ListBuffer class from the Scala Standard Library
Notice that in the body of the examples object that you no longer have to explicitly type out math.pow or math.round, you simply have direct access to the methods of the math package since you have imported all the methods of that package using the underscore wildcard. The same is true for the ListBuffer class. While this example is somewhat futile in terms of the end results of the list of fruit since it simply moves the strings from one type of list to another, more mutable, type of list, it does serve to provide an example of how to import and use a ListBuffer without needing to explicitly type out the entire namespace each time you use it.
Note
It is possible to run into method name collisions if you attempt to import methods from multiple packages with the same name. If this occurs in your code, you can alias a particular method using the arrow operator encapsulated in curly braces (i.e., Math.{pow => power}).
Exercise 9-1
- 1.
Go to www.scala-lang.org/api/current to browse the list of Scala standard library classes, objects, and methods. See if you can use any of these in your current code to simplify any of the code you have already written.
- 2.
For all code examples you have followed up to this point, go back and remove any explicitly typed out namespaces and instead import the method or object you need at the top of your file.
Application
Now that you have seen how to split code into several files for further modularity and organization, let’s apply that knowledge to our Nebula operating system shell. We’ll start by defining a package at the top of our main script called os.nebula. It is a best practice to use package names that coincide with top-level web domains that you own so that when the broader community uses your code, they will not encounter any namespace collisions. For example, if you owned the top-level domain www.ilovescala.com , the standard convention would be to use the package name com.ilovescala.mypackagename. In this case since we are just creating example code that will not be used by other developers, we can resign to simply using our os.nebula namespace with no concern for collisions.
Isolating the TextFile case class into its own file
Utilities.scala file that contains all of the command functions
Wrapping the main script in an object that extends App
Exercise 9-2
Now that all of the files have been separated, run your nebula program with the command scala os.nebula.nebula (the package name followed by the object name that extends App). Test to ensure everything works the same as it did previously.
Try to identify opportunities to further abstract this program into separate files for better organization. See if you can figure out how to create sub-directories to organize these files in.
Implementing a write command that leverages the Java standard library
Notice that in the first line of the Utilites.scala file, we are importing the java.io.PrintWriter function that allows us to write actual files to the operating system that our shell will be running on. We also added a write command to the list of commands that the shell can take as user input in the main nebula.scala file. This new command provides a new user experience that differs from our original make command. Instead of having the function parse the user input by splitting the command on a forward slash, it assumes that there will be no spaces in the file name and therefore only takes the command name and the file name as the original input. Then, once the function has kicked off, it prompts the user on a separate line to provide the text input that they wish to add to their .txt file. The user then enters the text they want to provide and hits enter. This then incites the program to save the text file and let the user know that the file is saving. The input from the second user prompt is stored in the textBody variable and passed into the PrintWriter function that we imported from Java. The PrintWriter then does the actual work of saving the .txt file to the file system of the computer.
An implementation of a List command to list out all text files in the current directory
Notice that imports can occur within the scope of functions in Scala as demonstrated by the import of the java.io.File class in this list function. This import is only accessible within the body of the function. If you tried to access the File class outside of this function, you would get an error. This is exceptionally useful when you do run into namespace collisions, particularly between the Scala standard library and the Java standard library, and you want to limit the scope of your import to avoid the collision.
Exercise 9-3
- 1.
See if you can identify other functionality from either the Scala standard library or the Java standard library that you can add to your shell. Perhaps you can wrap the Math library to provide additional functionality for arithmetic operations.
- 2.
Try to add a command to edit an existing text file in your operating system. What will the user experience be like? Is it easier to do this given functionality from the Java standard library?
Summary
In this chapter, you learned about splicing your Scala code into individual files while still maintaining continuity between them using packages. You also learned how to import compiled objects for methods that do not exist in your package. Building on that knowledge, you were shown how to import methods and classes from both the Scala and the Java standard libraries. All of this knowledge allows you to now organize your code for easier digestion which will, in turn, facilitate more productive collaboration. In the next chapter, you will be introduced to several programming paradigms that will build off of the fact that you can now separate your code into different files.