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

9. Dependency Management

Jason Lee Hodges1 
(1)
Draper, UT, USA
 

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

The basic object in Scala is a structure that allows code to be encapsulated without the need to instantiate it like a class. It is often useful to use a simple object when creating your first compiled program because you can wrap your entire program in a single object with very minimal boilerplate code. Listing 9-1 demonstrates how you might create a simple “Hello World” program in Scala, wrap it in an object, compile it, and then execute it.
examples.sc
object examples extends App {
       println("Hello World")
}
Terminal Output
> scalac examples.sc
> scala examples
Hello World!
Listing 9-1

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

Packages are a great way to organize code under a single namespace across multiple files. Files that all belong to the same package have access to the members of the other files within the package. This allows you to break up your code into multiple files without needing to worry about rewriting pieces of common code across those files. All you need to do to group Scala class files together as a package is to include the package keyword followed by the common package name that you choose to use at the top of each of your files. Listing 9-2 demonstrates the creation of two Scala files grouped in the same package that have access to each other’s members.
messenger.scala
package examples
object messenger extends App {
    println(messages.hello)
}
messages.scala
package examples
object messages {
    var hello = "Hello World!"
    var question = "How are you?"
    var farewell = "Regards."
}
Terminal Output
> scalac messenger.scala messages.scala
> scala examples.messenger
Hello World!
Listing 9-2

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

There will be times in the development and organization of a program that you do not wish for all of the files or objects that you create to live in the same package. Perhaps they need to be logically divided or sub-divided into different package names to make it easier for collaborating developers to understand how to obtain or navigate to the appropriate piece of code that they wish to reference. Or perhaps you’ve created a utility that might be really useful in other projects so you want to abstract it out into its own package for later use. In this scenario, in order to reference a member of an object that lives in a different package, you can explicitly import that member or package into your code using the import keyword. Listing 9-3 demonstrates an explicit import.
messenger.scala
import stringWrapper._
package examples {
    object messenger extends App {
        println(stringWrapper.userMessage("John Doe", messages.hello))
    }
}
stringWrapper.scala
object stringWrapper {
    def userMessage(userName: String, message: String): String = {
        return s"${userName} says: ${message}"
    }
}
Terminal Output
> scalac messenger.scala stringWrapper.scala
> scala examples.messenger
John Doe says: Hello World!
Listing 9-3

Explicitly importing a stringWrapper object to the messenger app

In this example, we are importing all of the members of the stringWrapper object into the messenger.scala file. This is denoted by the underscore wildcard after the stringWrapper import statement. If you only wanted to import a single method from the object, you would explicitly add the method name instead of the underscore. If you wanted to import multiple methods but not all methods, you would list them inside curly braces after the import statement. Listing 9-4 shows an example of this. The stringWrapper class in this example simply takes a username and a message and returns a string that wraps those two parameters so that the downstream consumer of this wrapper knows who was providing the message (like the signature of an email or the user icon in a chat or comment box). You can see how this stringWrapper might be useful in more than one program which is why you might not want to keep it exclusively in the examples package.
import objectname.{method1, method2, method3}
Listing 9-4

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

You may have noticed in previous chapters that sometimes we used classes or data types from the Scala standard library that have really long namespaces. While this is fine if you only need to type the entire namespace once, it gets exceptionally verbose if you need to repeat that code all throughout your program. In this scenario, you can simply add an import statement to the top of your file and you then have access to that module namespace anywhere within the scope of the program that you have imported the module in to. In the chapter on expressions, there were several methods that were used as part of the Scala Math package that could have been cleaned up. Also, in the previous chapter, our Nebula Operating System example used a ListBuffer from the Scala standard library as a means for storing any files that might be added dynamically by the user during runtime. Listing 9-5 provides an illustration on how those packages could be imported using our examples object.
examples.scala
import math._
import scala.collection.mutable.ListBuffer
object examples extends App {
    println(pow(3,2))
    println(round(2.45))
    var splitList = "apples, oranges, bananas".split(",")
    var fruit = new ListBuffer[String]
    for(i <- Range(0,splitList.length)){
        fruit += splitList(i)
    }
    println(fruit)
}
Terminal Output
9.0
2
ListBuffer(apples, oranges, bananas)
Listing 9-5

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. 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. 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.

Next, let’s create two additional files. One called TextFile.scala and another called Utilities.scala. It is a best practice when organizing your code to put individual classes, whether they be case classes or otherwise, into their own files. Typically, classes that model data (like our TextFile case class) are often organized into a sub-folder called “Models,” but we will just keep everything at the top level for now for the sake of simplicity. In both of these files, add the package name at the top so that members can be shared between these files. Now we can move our case class declaration into the TextFile.scala file. Listing 9-6 demonstrates this move.
TextFile.scala
package os.nebula
case class TextFile(title: String, text: String)
Listing 9-6

Isolating the TextFile case class into its own file

Next, we can move all of our command functions into our Utilities.scala file. They will need to be wrapped in a basic object so that they can be compiled and called statically. Let’s call that object Utilities. Back in your main script, any reference to these functions needs to have the “Utilities” object name prepended so that our Scala file knows that it needs to look in that object to find them. Listing 9-7 shows an example of this new Utilities.scala file.
Utilities.scala
package os.nebula
object Utilities {
    def addCommand(userInput: String) {
        ...
    }
    var files = new scala.collection.mutable.ListBuffer[TextFile]()
    def createTextFile(userInput: String) {
        ...
    }
    def showTextFiles(){
        ...
    }
}
Listing 9-7

Utilities.scala file that contains all of the command functions

Finally, we need to wrap our main script in an object that extends App so that Scala knows that it is the entry point for our program. Listing 9-8 demonstrates this as well as the addition of the Utilities object name to our function calls. Once these files have all been created, you will need to compile them all using the command scalac nebula.scala TextFile.scala Utilities.scala.
nebula.scala
package os.nebula {
    object nebula extends App {
        println("Welcome to the Nebula Operating System (NOS)! Version 1.0.5")
        var command = ""
        do {
            command = readLine("NOS> ")
            command match {
                case c if c.contains("make") => Utilities.createTextFile(c)
                case c if c.contains("show") => Utilities.showTextFiles()
                case c if c.contains("+") => Utilities.addCommand(c)
                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 9-8

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.

In addition to organizing your code, the added knowledge of understanding imports and the power of compiling your code to Java provide new possibilities to our Nebula terminal. Let’s create a new command that writes an actual text (.txt) file using an import from the Java standard library. Listing 9-9 details the implementation of this endeavor. We could technically replace our createTextFile function with this new function; however, it is a good example of other Scala functionality so we will leave it in for the time being.
nebula.scala
package os.nebula {
    object nebula extends App {
        println("Welcome to the Nebula Operating System (NOS)! Version 1.0.5")
        var command = ""
        do {
            command = readLine("NOS> ")
            command match {
                case c if c.contains("write") => Utilities.write(c)
                ...
            }
        }
        while (!command.equalsIgnoreCase("shutdown"))
    }
}
Utilities.scala
import java.io.PrintWriter
package os.nebula {
    object Utilities {
        ...
        def write(userInput: String){
            var tokens = userInput.split(" ")
            println("[Enter the text you wish to write to your new file below]")
            var textBody = readLine()
            try{
                new PrintWriter(s"${tokens(1).trim}.txt") { write(textBody); close }
                println("[Saving File...]")
            }
            catch {
                case _: Throwable =>  println("An error occurred trying to write a text file.")
            }
        }
}
Listing 9-9

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.

Remember to compile your nebula.scala file and the Utilities.scala file again and then re-run your shell to test it out. You’ll notice that when you successfully run this command, a new text file with the name you provided will be written to the same directory that your shell is being run from. Listing 9-10 provides an example of how you would implement another function to list out all text files that have been created in this same directory.
def list(){
    import java.io.File
    var dir = new File("./").listFiles
    for(file <- Range(0,dir.length)){
        if(dir(file).getName.contains("txt")){
            println(dir(file))
        }
    }
}
Listing 9-10

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. 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. 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.

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

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