Chapter 1. Zero to Sixty: Introducing Scala

Let’s start with a brief look at why you should investigate Scala. Then we’ll dive in and write some code.

Why Scala?

Scala is a language that addresses the needs of the modern software developer. It is a statically typed, object-oriented and functional, mixed-platform language with a succinct, elegant, and flexible syntax, a sophisticated type system, and idioms that promote scalability from small, interpreted scripts to large, sophisticated applications. So let’s consider each of those ideas in more detail:

A JVM, JavaScript, and native language

Scala started as a JVM language that exploits the performance and optimizations of the JVM, as well as the rich ecosystem of tools and libraries built around Java. More recently, Scala.js brings Scala to JavaScript and Scala Native is an experimental Scala that compiles to native machine code, bypassing the JVM and JavaScript runtimes.

Statically typed

Scala embraces static typing as a tool for creating robust applications. It fixes many of the flaws of Java’s type system and it uses type inference to eliminate much of the typing boilerplate.

Object-oriented programming

Scala fully supports object-oriented programming (OOP). Scala improves Java’s object model with the addition of traits, providing a clean way to implement types using mixin composition. In Scala, everything really is an object, even numeric types, providing much more consistent handling, especially in collections.

Functional programming

Scala fully supports functional programming (FP). FP has emerged as the best tool for thinking about problems of concurrency, Big Data, and general code correctness. Immutable values, first-class functions, functions without side effects, “higher-order” functions, and functional collections all contribute to concise, powerful, and correct code.

A sophisticated type system

Scala extends the type system of Java with more flexible generics and other enhancements to improve code correctness. With type inference, Scala code is often as concise as code in dynamically typed languages, yet inherently safer.

A succinct, elegant, and flexible syntax

Verbose expressions in Java become concise idioms in Scala. Scala provides several facilities for building domain-specific languages (DSLs), APIs that feel “native” to users.1

Scalable—architectures

You can write small, interpreted scripts to large, distributed applications in Scala.

The name Scala is a contraction of the words scalable language. It is pronounced scah-lah, like the Italian word for “staircase.” Hence, the two “a”s are pronounced the same.

Scala was started by Martin Odersky in 2001. The first public release was January 20th, 2004. Martin is a professor in the School of Computer and Communication Sciences at the Ecole Polytechnique Fédérale de Lausanne (EPFL). He spent his graduate years working in the group headed by Niklaus Wirth, of Pascal fame. Martin worked on Pizza, an early functional language on the JVM. He later worked on GJ, a prototype of what later became Generics in Java, along with Philip Wadler, one of the designers of Haskell. Martin was hired by Sun Microsystems to produce the reference implementation of javac, the descendant of which is the Java compiler that ships with the Java Developer Kit (JDK) today.

The Seductions of Scala

The growth of Scala users since it was introduced over fifteen years ago confirms my view that Scala is a language for our time. You can leverage the maturity of the JVM and JavaScript ecosystems while enjoying state-of-the-art language features with a concise, yet expressive syntax for addressing today’s development challenges.

In any field of endeavor, the professionals need sophisticated, powerful tools and techniques. It may take a while to master them, but you make the effort because mastery is the key to your success.

I believe Scala is a language for professional developers. Not all users are professionals, of course, but Scala is the kind of language a professional in our field needs, rich in features, highly performant, expressive for a wide class of problems. It will take you a while to master Scala, but once you do, you won’t feel constrained by your programming language.

Why Scala 3?

If you used Scala before, you used Scala 2, the major version since March 2006! Scala 3 aims to improve Scala in several ways.

First, Scala 3 strengthens Scala’s foundations, especially in the type system. Martin Odersky and collaborators have been developing the dependent object typing (DOT) calculus, which provides a more sound foundation for Scala’s type system. Scala 3 integrates DOT.

Second, Scala 2 has many powerful features, but sometimes they can be hard to use. Scala 3 improves the usability and safety of these features, especially implicits. Other language warts and “puzzlers” are removed.

Third, Scala 3 improves the consistency and expressiveness of Scala’s language constructs and it removes unimportant constructs to make the language smaller and more regular. The previous experimental approach to macros is replaced with a principled approach to meta-programming.

We’ll call out these changes as we explore the corresponding language features.

Migrating to Scala 3

The Scala team has worked hard to make migration to Scala 3 from Scala 2 as painless as possible, while still allowing the language to make improvements that require breaking changes. Scala 3 uses the same collections library as Scala 2.13. Hence, if you are using a Scala 2 version earlier than 2.13, I recommend upgrading to Scala 2.13 first, to update uses of the library, then upgrade to Scala 3.

However, to make this transition as painless as possible, there are several ways to compile Scala code that allows or disallows deprecated Scala 2 constructs. There are even compiler flags that will do some rewrites for you. See Scala3Versions and CommandLineToolScalac in ScalaTools for details.

Installing Scala

Let’s learn how to install the command-line tools that you need to work with the book’s code examples.2 The examples used in this book were written and compiled using Scala version 3.0, the latest release at the time of this writing.

All the examples will use Scala on the JVM. See the Scala.js and Scala Native websites for information on targeting those platforms.

At a minimum, you need to install a recent Java JDK and the de facto build tool for Scala, SBT. Then you can use the sbt command to bootstrap everything else for the examples. However, the following sections go into more details about tools you might want to install and how to install them.

The Scala website Getting Started page discusses even more options to get started with Scala.

Java JDK

Use a recent Java JDK release. Version 11 or newer is recommended, although Java 8 should work. To install the JDK, go to the Oracle Java website and follow the instructions to install the full Java Development Kit (JDK).

SBT

The most popular build tool for Scala, SBT, version 1.3.8 or newer, is used for the code examples. Install using Coursier or follow the instructions at scala-sbt.org.

When you are finished, you will have an sbt command that you can run from a Linux or OS X terminal or Windows command window.

Scala

You actually don’t need to install the Scala command-line tools separately, as SBT will install the basics that you need as dependencies. However, if you want to install these tools, use Coursier or see the instructions at scala-lang.org/download.

Building the Code Examples

Now that you have the tools you need, you can download and build the code examples.

Get the Code

Download the code examples as described in GettingCodeExamples.

Start SBT

Open a terminal and change to the root directory for the code examples. Type the command sbt test. It will download all the library dependencies you need, including the Scala compiler. This will take a while and you’ll need an Internet connection. Then sbt will compile the code and run the unit tests. You’ll see lots of output, ending with a “success” message. If you run the command again, it should finish very quickly because it won’t need to do anything again.

Congratulations! You are ready to get started.

Tip

For most of the book, we’ll use the Scala tools indirectly through SBT, which downloads the Scala compiler version we want, the Scala interpreter, Scala’s standard library, and the required third-party dependencies automatically.

More Tips

In your browser, it’s useful to bookmark the URL for the Scala library’s Scaladocs, the analog of Javadocs for Scala. For your convenience, most times when I mention a type in the Scala library, I’ll include a link to the corresponding Scaladocs entry.

Use the search field at the top of the page to quickly find anything in the docs. The documentation page for each type has a link to view the corresponding source code in Scala’s GitHub repository, which is a good way to learn how the library was implemented. Look for the link on the line labeled “Source.”

Any text editor or IDE (integrated development environment) will suffice for working with the examples. Scala plug-ins exist for all the popular editors and IDEs. For more details, see IntegrationWithIDEs. In general, the community for your favorite editor is your best source of up-to-date information on Scala support.

Using SBT

Let’s cover the basics of using SBT, which you’ll need to work with the code examples.

When you start the sbt command, if you don’t specify a task to run, SBT starts an interactive REPL (Read, Eval, Print, Loop). Let’s try that now and see a few of the available “tasks.”

In the listing that follows, the $ is the shell command prompt (e.g., bash), where you start the sbt command, the > is the default SBT interactive prompt, and the # starts an sbt comment. You can type most of these commands in any order:

$ sbt
> help      # Describe commands.
> launchIDE # Open the project in Visual Studio Code.
> tasks     # Show the most commonly-used, available tasks.
> tasks     # Show ALL the available tasks.
> compile   # Incrementally compile the code.
> test      # Incrementally compile the code and run the tests.
> clean     # Delete all build artifacts.
> console   # Start the Scala REPL.
> run       # Run one of the "main" routines in the project.
> show      # Show the definition of variable "x".
> exit      # Quit the REPL (also control-d works).
Tip

The SBT project for the code examples is actually configured to show the following as the SBT prompt:

sbt:Programming Scala, Third Edition - Code examples>

We’ll use the more concise prompt, >, the default for SBT, to save space.

The launchIDE task is convenient for those of you who prefer IDEs, although currently only Visual Studio Code is supported. The details can be found at dotty.epfl.ch/docs/usage/ide-support.html.

Both IntelliJ IDEA and Visual Studio Code can open SBT project, once you install a Scala plug-in.

A handy SBT technique is to add ~ at the front of any command. Whenever file changes are saved to disk, the command will be rerun. For example, I use ~test all the time to keep compiling amd running my code and tests. SBT uses an incremental compiler, so you don’t have to wait for a full rebuild every time. Break out of this loop by hitting the return key.

Scala has its own REPL. Invoke it using the console command in SBT. You will use this a lot to try examples in the book. The Scala REPL prompt is scala>.

Before starting the REPL, SBT will build your project and set up the CLASSPATH with your built artifacts and depedent libraries. This convenience means it’s rare to use the scala command-line tool outside of SBT.

You can exit both the SBT REPL and the Scala REPL with Ctrl-D.

Tip

Using the Scala REPL is a very effective way to experiment with code idioms and to learn an API, even Java APIs. Invoking it from SBT using the console task conveniently adds project dependencies and the compiled project code to the classpath for the REPL.

Running the Scala Command-Line Tools

If you installed the Scala command-line tools separately, the Scala compiler is called scalac, analogous to the Java compiler javac. We will let SBT run it for us, but the command syntax is straightforward if you’ve ever run javac. Use scalac -help to see the options.

Similarly, the scala command, which is similar to java, is used to run programs, but it also supports the interactive REPL mode we just discussed and the ability to run Scala “scripts”. Consider this example script from the code examples:

// src/script/scala/progscala3/introscala/Upper1.scala

class Upper1:
  def convert(strings: Seq[String]): Seq[String] =
    strings.map((s: String) => s.toUpperCase())

val up = new Upper1()
println(up.convert(List("Hello", "World!")))

Let’s run it with the scala command. Change your current working directory to the root of the code examples. For Windows, use backslashes in the next command:

$ scala src/script/scala/progscala3/introscala/Upper1.scala
List(HELLO, WORLD!)
...

And thus we have satisfied the Prime Directive of the Programming Book Authors Guild, which states that our first program must print “Hello World!”

Tip

As you can see from the listing above for Upper1.scala, each of these files is listed starting with a comment that contains the file path in the code examples. That makes it easy to find the file.

If you invoke scala without a compiled main class to run or a script file, scala enters the REPL mode. Here is a REPL session illustrating some useful commands. If you didn’t install Scala separately, just start console in sbt. The REPL prompt is scala> (some output elided):

$ scala
...
scala> :help
The REPL has several commands available:

:help                    print this summary
:load <path>             interpret lines in a file
:quit                    exit the interpreter
:type <expression>       evaluate the type of the given expression
:doc <expression>        print the documentation for the given expresssion
:imports                 show import history
:reset                   reset the repl to its initial state, ...

scala> val s = "Hello, World!"
val s: String = Hello, World!

scala> println("Hello, World!")
Hello, World!

scala> 1 + 2
val res0: Int = 3

scala> s.con<tab>
concat   contains   containsSlice   contentEquals

scala> s.contains("el")
val res1: Boolean = true

scala> :quit
$    # back at the terminal prompt.

We assigned a string, "Hello, World!", to a variable named s, which we declared as an immutable value using the val keyword. The println function prints a string to the console, followed by a line feed.

This println is effectively the same thing as Java’s System.out.println. Also, Scala Strings are Java Strings.

When we added two numbers, we didn’t assign the result to a variable, so the REPL made up a name for us, res0, which we could use in subsequent expressions.

The REPL supports tab completion. The input shown is used to indicate that a tab was typed after s.con. The REPL responded with a list of methods on String that could be called. The expression was completed with a call to the contains method.

We didn’t always explicit specify type information. When type information is shown, either when it is inferred or explicit type information is added to declarations, these type annotations, as they are called, follow a colon after the item name. The output of REPL shows several examples.

Why doesn’t Scala follow Java conventions? When type annotations aren’t explicitly in the code, then the type is inferred. Compared to Java’s type item convention, the item: type convention is easier for the compiler to analyze unambiguously when you omit the type annotation and just write item.

As a general rule, Scala follows Java conventions, departing from them for specific reasons, like supporting a new feature that would be difficult using Java syntax.

Tip

Showing the types in the REPL is very handy for learning the types that Scala infers for particular expressions. It’s one example of exploration that the REPL enables.

Finally, we used :quit to exit the REPL. Ctrl-D can also be used.

We’ll see additional REPL commands as we go and we’ll explore the REPL commands in depth in CommandLineTools.

A Taste of Scala

We’ve already seen a bit of Scala as we discussed tools, including how to print “Hello World!”. The rest of this chapter and the two chapters that follow provide a rapid tour of Scala features. As we go, we’ll discuss just enough of the details to understand what’s going on, but many of the deeper background details will have to wait for later chapters. Think of this tour as a primer on Scala syntax and a taste of what programming in Scala is like day to day.

Tip

When we mention a type in the Scala library, you might find it useful to read more in the Scaladocs about it. The Scaladocs for the current release of Scala can be found here. Note that Scala 3 uses the Scala 2.13 collections library, while other parts of the library have changed in Scala 3.

All examples shown in the book start with a comment line like this:

// src/script/scala/progscala3/introscala/Upper1.scala

Scala follows the same comment conventions as Java, C#, C, etc. A // comment goes to the end of a line, while a /* comment */ can cross line boundaries. Scaladoc comments follow Java conventions, /** comment */.

When the path starts with src/script, use scala to run the script, as follows:

$ scala src/script/scala/progscala3/introscala/Upper1.scala

However, this may not work if the script uses libraries. Instead, run the SBT console, then use the :load command:

scala> :load src/script/scala/progscala3/introscala/Upper1.scala

Finally, you can also copy code and paste it at the scala> prompt.

Files named src/test/scala/.../*Suite.scala are tests written using MUnit (see _testing_tools). To run all the tests, use the sbt command test. To run just one particular test, use testOnly path, where path is the fully-qualified type name for the test.

> testOnly progscala3.objectsystem.equality.EqualitySuite
[info] Compiling 1 Scala source to ...
progscala3.objectsystem.equality.EqualitySuite:
  + The == operator is implemented with the equals method 0.01s
  + The != operator is implemented with the equals method 0.001s
  ...
[info] Passed: Total 14, Failed 0, Errors 0, Passed 14
[success] Total time: 1 s, completed Feb 29, 2020, 5:00:41 PM
>

The corresponding source file is src/test/scala/progscala3/objectsystem/equality/EqualitySuite.scala. SBT follows Maven conventions that directories for compiled source code go under src/main/scala and tests go under src/test/scala. So, in this example, the package definition for this test is progscala3.objectsystem.equality. The compiled class name is EqualitySuite.

Note

Java requires that package and file names must match the declared package and public class declared within the file. Scala doesn’t require this practice. However, I follow these conventions most of the time for compiled code (less often for scripts) and I recommend you do this, too, for your production code.

Finally, many of the files under the src/main/scala define a main method that you can execute in one of several ways.

First, use SBT’s run command. It will find all the classes with main methods and prompt you to pick which one. Note that SBT will only search src/main/scala and src/main/java directories, ignoring the other directories under src, including src/script.

Let’s use another example we’ll study later in the chapter, src/main/scala/progscala3/introscala/UpperMain1.scala. Invoke run hello world, then enter the number shown for progscala3.introscala.UpperMain1. Note we are passing arguments to run, hello world, which will be passed to the program to convert to upper case:

> run hello world
...

Multiple main classes detected, select one to run:

 ...
 [20] progscala3.introscala.UpperMain1
 ...

20

[info] running progscala3.introscala.UpperMain1 hello world
HELLO WORLD
[success] Total time: 2 s, completed Feb 29, 2020, 5:08:18 PM

The second way to run this program is to use runMain and specify the specific program class name. This skips the prompt:

> runMain progscala3.introscala.UpperMain1 hello world
[warn] Multiple main classes detected.  Run 'show discoveredMainClasses' ...
[info] running progscala3.introscala.UpperMain1
HELLO WORLD
[success] Total time: 0 s, completed Feb 29, 2020, 5:18:05 PM
>

Finally, once your program is ready for production runs, you’ll use the scala command, similar to how java is used. Now the correct classpath must be defined, including all dependencies. This example is relatively easy; we just point to the output directory for the compiled code:

$ scala -cp target/scala-3.0.0/classes/ progscala3.introscala.UpperMain1 words

Let’s explore other differences between scripts, like the Upper1 script we’ve used, and compiled code, like the UpperMain1 example we just executed.

Here is the script again:

// src/script/scala/progscala3/introscala/Upper1.scala

class Upper1:
  def convert(strings: Seq[String]): Seq[String] =
    strings.map((s: String) => s.toUpperCase())

val up = new Upper1()
println(up.convert(List("Hello", "World!")))

We declare a class, Upper1, using the class keyword. The entire class body is indented on the subsequent lines (or inside curly braces {…} if you use that syntax instead).

Upper1 contains a method called convert. Method definitions start with the def keyword, followed by the method name and an optional parameter list. The method signature ends with an optional return type. The return type can be inferred in many cases, but adding the return type explicitly, as shown, provides useful documentation and also avoids occasional surprises from the type inference process.

Note

I’ll use parameters to refer to the things a method or function is defined as accepting when you call it. I’ll use arguments to refer to values you actually pass to it when making the call.

Type annotations are specified using name: type. This is used here for both the parameter list and the return type of the method, the last Seq[String] before the equals sign.

An equals sign (=) separates the signature from the method body. Why an equals sign?

One reason is to reduce ambiguity. Scala infers the return type if the colon and type are omitted. If the method takes no parameters, you can omit the parentheses, too. So, the equal sign makes parsing unambiguous when either or both of these features are omitted. It’s clear where the signature ends and the method body begins.

The equals sign also reminds us of the functional programming principle that variables and functions are treated uniformly. As we saw in the invocation of map, functions can be passed as arguments to other functions, just like values. They can also be returned from functions, and assigned to variables. In fact, it’s correct to say that functions are values.

This method takes a sequence (Seq) of zero or more input strings and returns a new sequence, where each of the input strings is converted to uppercase. Seq is an abstraction for collections that you can iterate through. The actual type returned by this method will be the same concrete type that was passed into it as an argument, like Vector or List (both of which are immutable collections).

Collection types like Seq are parameterized types, very similar to generic types in Java. They are a “collection of something,” in this example a sequence of strings. Scala uses square brackets ([…]) for parameterized types, whereas Java uses angle brackets (<…>).

Note

Scala allows angle brackets to be used in identifiers, like method and variable names. For example, defining a “less than” method and naming it < is common and allowed by Scala, whereas Java doesn’t allow characters like that in identifiers. So, to avoid ambiguity, Scala uses square brackets instead for parameterized types and disallows them in identifiers.

Inside the body, we use one of the powerful methods available for most collections, map, which iterates through the collection, calls the provided method on each element, and returns a new collection with the transformed elements.

The function passed to map is an unnamed function literal (parameters) => body, similar to Java’s lambda syntax:

(s: String) => s.toUpperCase()

It takes a parameter list with a single String named s. The body of the function literal is after the “arrow,” =>. (The UTF8 ⇒ characters was also allowed in Scala 2, but is now deprecated.) The body calls toUpperCase() on s. The result of this call is automatically returned by the function literal. In Scala, the last expression in a function or method is the return value. The return keyword exists in Scala, but it can only be used in methods, not in anonymous functions like this one. In fact, it is rarely used in methods.

On the JVM, functions are implemented using JVM lambdas:

scala> (s: String) => s.toUpperCase()
val res0: String => String = Lambda$7775/0x00000008035fc040@7673711e=

The last two lines create an instance of Upper1, named up, and use it to convert two strings to uppercase and finally print the resulting Seq. As in Java, the syntax new Upper1() creates a new instance. The up variable is declared as a read-only “value” using the val keyword. It behaves like a final variable in Java.

Now lets look at the compiled example, where I added Main to the name. Note the path to the source file now contains src/main, instead of src/script:

// src/main/scala/progscala3/introscala/UpperMain1.scala
package progscala3.introscala                                   1

object UpperMain1:
  def main(params: Array[String]): Unit =                         2
    params.map(s => s.toUpperCase()).foreach(s => printf("%s ",s))
    println("")
  end main                                                      3

  @main def hello(params: String*) = main(params.toArray)       4
end UpperMain1                                                  5
1

Declare the package location.

2

Declare a main method, the program entry point.

3

For long methods (unlike this one), you can use end name, but this is optional.

4

An alternative way to define an entry point method.

5

Optional end to the object definition.

Packages work much like they do in Java. Packages provide a “namespace” for scoping. Here we specify that this class exists in the progscala3.introscala package.

To declare a main method in Java, you would put it in a class and declare it static, meaning not tied to any one instance. You can then call it with the syntax MyClass.main. This pattern is so pervasive, that Scala builds it into the language. We instead declare an object, named UpperMain1, using the object keyword, then declare main as we would any other method. At the JVM level, it looks like a static method. When running this program like we did above, this main method is invoked.

Note the parameter list is an Array[String] and it returns Unit, which is analogous to void in programs like Java, where nothing useful is returned.

You use end name with all of the constructs like method and type definitions, if, while expressions, etc. that support the braceless, indentation-based structure. It is optional, intended for long definitions where it can be hard to see the beginning and end at the same time. Hence, for this short definition of main, you wouldn’t use it normally. Here are other examples for control constructs:

if sequence.length > 0 then
  println(sequence)
end if      // "end if" is optional

while i < 10 do
  i += 1
end while   // "end while" is optional

The end … lines shown are not required, but they are useful for readability if the indented blocks are many lines long.

The @main def hello method is another way to declare an entry point, especially useful when you don’t need to process arguments, so you don’t need to declare the parameter list. Note that in the list printed by the sbt run command, this entry point is shown as progscala3.introscala.hello, while the main method was shown by the type name, progscala3.introscala.UpperMain1. Try run and invoke hello.

Declaring UpperMain1 as an object makes it a singleton, meaning there will always only be one instance of it, controlled by the Scala runtime library. You can’t create your own instances with new.

Scala makes the Singleton Design Pattern a first-class member of the language. In most ways, these object declarations are just like class declarations. Scala uses these objects to replace “class-level” members, like statics in Java. You use the declared object like an instance created from a regular class, reference the member variables and functions as required, e.g., UpperMain1.hello.

When you define the main method for your program, you must declare it inside an object, but in general, UpperMain1 is a good candidate for a singleton, because we don’t need more than one instance and it carries no state information.

The Singleton Design Pattern has drawbacks. It’s hard to replace a singleton instance with a test double in unit tests and forcing all computation through a single instance raises concerns about thread safety and performance. However, there are times when singletons make sense, as in this example where these concerns don’t apply.

Note

Why doesn’t Scala support statics? Compared to languages that allow static members (or equivalent constructs), Scala is more true to the vision that everything should be an object. The object construct keeps this policy more consistent than in languages that mix static and instance class members.

UpperMain1.main takes the user arguments in the params array, maps over them to convert to upper case, then uses another common method, foreach, to print them out.

The function we passed to map drops the type annotation for s:

s => s.toUpperCase()

Most of the time, Scala can infer the types of parameters for function literals, because the context provided by map tells the compiler what type to expect.

The foreach method is used when we want to process each element and do something with complete side effects, without returning a new value, unlike map. Here we print a string to standard out, without a newline after each one. The last println call prints the newline before the program exits.

The notion of side effects means that the function we pass to foreach does something to affect state outside the local context. We could write to a database or to a file system, or print to the console, as we do here.

Look again at the first line inside main, how concise it is, where we compose operations together. Sequencing transformations together lets us create concise, powerful programs, as we’ll see over and over again.

We haven’t needed to “import” any library items yet, but Scala imports work the same was as they do in Java. Scala automatically imports many commonly used types and features, like the Seq and List types above and methods for I/O, like println, a method on the scala.Console object.

In Java, to use println, you have to write System.out.println or import System.out and then write out.println. In Scala, you can import objects and even individual methods from them. This is done automatically for you for Console.println, so we can just use println by itself. This method is one of many methods and types imported automatically that are defined in a library object called Predef.

To run this code, you must first compile to a JVM-compatible .class file using scalac. (Sometimes multiple class files are generated.) SBT will do this for you, but let’s so how to do it yourself. If you installed the Scala command-line tools separately, open a terminal window and change the working directory to the root of the project. Then run the following command (ignoring the $ prompt):

$ scalac src/main/scala/progscala3/introscala/UpperMain1.scala

You should now have a new directory named progscala3/introscala that contains several .class and .tasty files, including a file named UpperMain1.class. (“Tasty” files are an intermediate representation generated by the compiler.) Scala must generate valid JVM byte code and files. For example, the directory structure must match the package structure.

Now, you can execute this program to process a list of strings. Here is an example:

$ scala -cp . progscala3.introscala.UpperMain1 Hello World!
HELLO WORLD!

The -cp . option adds the current directory to the search classpath, although this is actually the default behavior.

Allowing SBT to compile it for us instead, we can run it at the SBT prompt using this command:

> runMain progscala3.introscala.UpperMain1 Hello World!

For completeness, if you compile with SBT, but run it using the scala command outside SBT, then set the classpath to point to the correct directory where SBT writes class files:

$ scala -cp target/scala-3.0.0/classes progscala3.introscala.UpperMain1 hello
HELLO

Returning to the script version, we can actually simplify it even more. Consider this simplified version:

// src/script/scala/progscala3/introscala/Upper2.scala

object Upper2:
  def convert(strings: Seq[String]) = strings.map(_.toUpperCase())

println(Upper2.convert(List("Hello", "World!")))

This code does exactly the same thing, but it’s more concise.

I omitted the return type for the method declaration, because it’s “obvious” what’s returned. However, we can’t omit the type annotations for parameters. Technically, the type inference algorithm does local type inference, which means it doesn’t work globally over your whole program, but locally within certain scopes. It can’t infer caller’s expectations for the parameter types, but it is able to infer the type of the method’s returned value in most cases, because it sees the whole function body. Recursive functions are one exception where the return type must be declared.

However, explicit type annotations in the parameter lists and explicit return type annotations provide useful documentation for the reader. Just because Scala can infer the return type of a function, should you let it? For simple functions, where the return type is obvious to the reader, perhaps it’s not that important to show it explicitly. However, sometimes the inferred type won’t be what’s expected, perhaps due to a bug or some subtle behavior triggered by certain input arguments or expressions in the function body. Explicit return types express what you think should be returned and the compiler confirms it. Hence, I recommend adding return types rather than inferring them, especially in public APIs.

We have also exploited a shorthand for the function literal. Previously we wrote it in the following two, equivalent ways:

(s: String) => s.toUpperCase()
s => s.toUpperCase()

We have now shortened it even further to the following expression:

_.toUpperCase()

The map method takes a single function parameter, where the function itself takes a single parameter. In this case, the function body only uses the parameter once, so we can use the anonymous variable _ instead of a named parameter. The string parameter will be assigned to it before toUpperCase is called.

Finally, using an object instead of a class simplifies the invocation, because we don’t need to first create an instance with new. We just call Upper2.convert directly.

Let’s do one last version of the compiled code (under src/main/scala) to show another way of working with collections for this situation:

// src/main/scala/progscala3/introscala/UpperMain2.scala
package progscala3.introscala

object UpperMain2:
  def main(params: Array[String]): Unit =
    val output = params.map(_.toUpperCase()).mkString(" ")
    println(output)

Instead of using foreach to print each transformed word as before, we map the array to a new array of strings, then call a convenience method to concatenate the strings into a final string. There are two mkString methods. One takes a single parameter to specify the delimiter between the collection elements. The second version that takes three parameters, a leftmost prefix string, the delimiter, and a rightmost suffix string. Try changing the code to use mkString("[", ", ", "]").

As a little exercise, return to the script Upper2.scala and try simplifying it further. Eliminate the Upper2 object completely and just call map on the list of words directly. You should have just one line of code when you’re done! (See the code examples for one implementation.)

A Sample Application

Let’s finish this chapter by exploring several more seductive features of Scala using a sample application. We’ll use a simplified hierarchy of geometric shapes, which we will “send” to another object for drawing on a display. Imagine a scenario where a game engine generates scenes. As the shapes in the scene are completed, they are sent to a display subsystem for drawing.

However, to keep it simple, this will be a single-threaded implementation. We won’t actually do anything with concurrency right now.

To begin, we define a Shape class hierarchy:

// src/main/scala/progscala3/introscala/shapes/Shapes.scala
package progscala3.introscala.shapes

case class Point(x: Double = 0.0, y: Double = 0.0)                    1

abstract class Shape():                                               2
  /**
   * Draw the shape.
   * @param f is a function to which the shape will pass a
   * string version of itself to be rendered.
   */
  def draw(f: String => Unit): Unit = f(s"draw: $this")               3

case class Circle(center: Point, radius: Double) extends Shape        4

case class Rectangle(lowerLeft: Point, height: Double, width: Double) 5
      extends Shape

case class Triangle(point1: Point, point2: Point, point3: Point)      6
      extends Shape
1

Declare a class for two-dimensional points.

2

Declare an abstract class for geometric shapes.

3

Implement a draw method for “rendering” the shapes. The documentation comment uses the same conventions that Java uses.

4

A circle with a center and radius.

5

A rectangle with a lower-left point, height, and width. We assume for simplicity that the sides are are parallel to the horizontal and vertical axes.

6

A triangle defined by three points.

Let’s unpack what’s going on.

The parameter list after the Point class name is the list of constructor parameters. In Scala, the whole class body is the constructor, so you list the parameters for the primary constructor after the class name and before the class body. In this case, there is no class body, so we can omit the colon (or curly braces if you use those instead).

Because we put the case keyword before the class declaration, each constructor parameter is automatically converted to a read-only (immutable) field of Point instances. That is, if you instantiate a Point instance named point, you can read the fields using point.x and point.y, but you can’t change their values. Attempting to use point.y = 3.0 triggers a compilation error.

You can also provide default values for method parameters, including constructors. The = 0.0 after each parameter definition specifies 0.0 as the default. Hence, the user doesn’t have to provide them explicitly, but they are inferred left to right.

You can also construct instances without using new.

Let’s use our SBT project to explore these points:

> console
[info] ...

scala> import progscala3.introscala.shapes._

scala> val p00 = Point()
val p00: progscala3.introscala.shapes.Point = Point(0.0,0.0)

scala> val p20 = Point(2.0)
val p20: progscala3.introscala.shapes.Point = Point(2.0,0.0)

scala> val p20b = Point(2.0)
val p20b: progscala3.introscala.shapes.Point = Point(2.0,0.0)

scala> val p02 = Point(y = 2.0)
val p02: progscala3.introscala.shapes.Point = Point(0.0,2.0)

scala> p20 == p20b
val res0: Boolean = true

scala> p20 == p02
val res1: Boolean = false

Running console will automatically compile the code first, so we don’t need to run compile first.

The import statement uses _ as a wildcard to import everything in the progscala3.introscala.shapes package. It behaves the same way as using * in Java. Scala uses _ because you might want to use * as a method name for multiplication and since Scala lets you import methods into the local scope, using * as a wildcard would be ambiguous! This is the second use for _ that we’ve seen. The first use was in function literals where _ was an anonymous placeholder for parameters, used instead of naming them.

In the definition of p00, no arguments are specified, so Scala used 0.0 for both of them. (However, you must provide the empty parentheses.) When one argument is specified, Scala applies it to the leftmost argument, x, and used the default value for the remaining argument, as shown for p20 and p20b. We can even specify the arguments with the associated parameter name. The definition of p02 uses the default value for x, but specifies the value for y, using Point(y = 2.0).

Tip

Using named arguments explicitly, even when it isn’t required, like Point(x = 0.0, y = 2.0), can make your code easier to understand.

While there is no class body for Point, another feature of the case keyword is the compiler automatically generates several methods for us, including the familiar toString, equals, and hashCode methods in Java. The output shown for each point, e.g., Point(2.0,0.0), is the default toString output. The equals and hashCode methods are difficult for most developers to implement correctly, so autogeneration of these methods is a real benefit. However, you can provide your own definitions for any of these methods, if you prefer.

When we asked if p20 == p20b and p20 == p02, Scala invoked the generated equals method. This is in contrast with Java, where == just compares references. In Java, you have to call equals explicitly to do a logical comparison.

The last feature of case classes that we’ll mention now is that the compiler also generates a companion object, a “singleton” of the same name, for each case class. In other words, we declared the class Point and the compiler also created an object Point.

Note

You can define companions yourself. Any time an object and a class have the same name and they are defined in the same file, they are companions.

The compiler also adds several methods to the companion object automatically, one of which is named apply. It takes the same parameter list as the constructor.

For any instance you use, either a declared object or an instance of a class, if you put an argument list after it, Scala looks for a corresponding apply method to call. Therefore, the following two lines are equivalent:

val p1 = Point.apply(1.0, 2.0)   // Point is the companion object here!
val p2 = Point(1.0, 2.0)

It’s a compilation error if no apply method exists for the instance. Also, the argument list supplied must conform to the expected parameter list.

The Point.apply method is effectively a factory for constructing Points. The behavior is simple here; it’s just like calling the Point class constructor without the new keyword. The companion object generated is equivalent to this:

object Point {
  def apply(x: Double = 0.0, y: Double = 0.0) = new Point(x, y)
  ...
}

You can add methods to the companion object. A more sophisticated apply method might instantiate a different subclass with specialized behavior, depending on the argument supplied. For example, a data structure might have an implementation that is optimal for a small number of elements and a different implementation that is optimal for a larger number of elements. The apply method can hide this logic, giving the user a single, simplified interface. Hence, putting an apply method on a companion object is a common idiom for defining a factory method for a class hierarchy, whether or not case classes are involved.

An instance apply method defined on any class has whatever meaning is appropriate for instances. For example, Seq.apply(index: Int) retrieves the element at position index (counting from zero).

Note

To recap, when an argument list is put after an object or +class instance, Scala looks for an apply method to call where the parameter list matches the argument list types. Syntacticly, anything with an apply method behaves like a function, e.g., Point(2.0, 3.0).

A companion object apply method is a factory method for the companion class instances. A class apply method has whatever meaning is appropriate for instances of the class, for example Seq.apply(index: Int) returns the item at position index.

Shape is an abstract class. We can’t instantiate an abstract class, even if none of the members is abstract. Shape.draw is defined, but we only want to instantiate concrete shapes: Circle, Rectangle, and Triangle.

The parameter f for draw is a function of type String => Unit. We saw Unit above. It is a real type, but it behaves roughly like void in other languages.3

The idea is that callers of draw will pass a function that does the actual drawing when given a string representation of the shape. For simplicity, we just use the string returned by toString, but a structured format like JSON would make more sense in a real application.

Tip

When a function returns Unit it is totally side-effecting. There’s nothing useful returned from the function, so it can only perform side effects on some state, like performing input or output (I/O).

Normally in functional programming, we prefer pure functions that have no side effects and return all their work as their return value. These functions are far easier to reason about, test, compose, and reuse. Side effects are a common source of bugs, so they should be used carefully.

Tip

Use side effects only when necessary and in well-defined places. Keep the rest of the code pure.

Shape.draw is another example that functions are first-class values, just like instances of Strings, Ints, Points, etc. We saw this previously with map and foreach. Like other values, we can assign functions to variables, pass them to other functions as arguments, and return them from functions. This is a powerful tool for building composable, yet flexible software.

When a function accepts other functions as parameters or returns functions as values, it is called a higher-order function (HOF).

You could say that draw defines a protocol that all shapes have to support, but users can customize. It’s up to each shape to serialize its state to a string representation through its toString method. The f method is called by draw and it constructs the final string using an interpolated string.

An interpolated string starts with s before the opening double quote: s"draw: ${this.toString}". It builds the final string by substituting the result of the expression this.toString into the larger string. Actually, we don’t need to call toString; it will be inferred, so we can use just ${this}. However, now we’re just referring to a variable, so we can drop the curly braces and just write $this. Hence, the expression becomes +s"draw: $this”.

Warning

If you forget the s before the interpolated string, you’ll get the literal output draw: $this, with no interpolation.

Back to the code listing, Circle, Rectangle, and Triangle are concrete subclasses of Shape. They have no class bodies, because the case keyword defines all the methods we need, such as the toString methods required by Shape.draw.

In our simple program, the f we will pass to draw will just write the string to the console, but you could build a real graphics application that uses an f to parse the string and render the shape to a display, write JSON to a web service, etc.

Even though this will be a single-threaded application, let’s anticipate what we might do in a concurrent implementation by defining a set of possible Messages that can be exchanged between modules.

// src/main/scala/progscala3/introscala/shapes/Messages.scala
package progscala3.introscala.shapes

sealed trait Message                                                 1
case class Draw(shape: Shape) extends Message                        2
case class Response(message: String) extends Message                 3
case object Exit extends Message                                     4
1

Declare a trait called Message. A trait defines an interface for behavior and can be used as an abstract base class, which is how we use it here. All messages exchanged are subclasses of Message. The sealed keyword is explained below.

2

A message to draw the enclosed Shape.

3

Response is used to return an arbitrary string message to a caller in response to a message received from the caller.

4

Exit has no state or behavior of its own, so it is declared a case object, since we only need one instance of it. It functions as a “signal” to trigger a state change, termination in this case.

The sealed keyword means that we can only define subclasses of Message in the same file. This prevents bugs where users define their own Message subtypes that could break the code in the next file! Note that Shape was not declared sealed previously, because we intend for people to create their own subclasses of it!

Now that we have defined our shapes and messages types, let’s define an object for processing messages:

// src/main/scala/progscala3/introscala/shapes/ProcessMessages.scala
package progscala3.introscala.shapes

object ProcessMessages:                                              1
  def apply(message: Message): Message =                             2
    message match                                                    3
      case Exit =>
        println(s"ProcessMessage: exiting...")
        Exit
      case Draw(shape) =>
        shape.draw(str => println(s"ProcessMessage: $str"))
        Response(s"ProcessMessage: $shape drawn")
      case Response(unexpected) =>
        val response = Response(s"ERROR: Unexpected Response: $unexpected")
        println(s"ProcessMessage: $response")
        response
1

If we only need one instance, we can declare it an object, but it would be easy enough to make this a class and instantiate as many as we need, for scalability, etc.

2

Define the apply method that takes a Message, processes it, then returns a new Message.

3

Match on the incoming message to determine what to do.

The apply method introduces a powerful feature call pattern matching:

def apply(message: Message): Message =
  message match:
    case Exit =>
      expressions
    case Draw(shape) =>
      expressions
    case Response(unexpected) =>
      expressions

The message match: ... expression consists only of case clauses, which do pattern matching on the message passed into the function. This is an expression that returns a value, which you can assign to a variable or use as the return value, as we do here for apply.

Match expressions work a lot like “if/else” expressions. When one of the patterns matches, the expressions are evaluated after the arrow (=>) up to the next case keyword (or the end of the definition). Matching is eager; the first match wins.

If the case clauses don’t cover all possible values that can be passed to the match expression, a MatchError is thrown at runtime. Fortunately, the compiler can detect many cases where the match clauses don’t handle all possible inputs. Note that our sealed hierarchy of messages is crucial here. If a user created a new subtype of Message, our match expression would no longer cover all possibilities. Hence, a bug would be introduced in this code!

A powerful feature of pattern matching is the ability to extract data from the object matched, sometimes called deconstruction (the inverse of construction). Here, when the input message is a Draw, we extract the enclosed Shape and assign it to the variable shape. Similarly, if Response is detected, we extract the message as unexpected, because ProcessMessages doesn’t expect to receive a Response!

Now let’s look at the expressions invoked for each case match:

def apply(message: Message): Message =
  message match
    case Exit =>                                                     1
      println(s"ProcessMessage: exiting...")
      Exit
    case Draw(shape) =>                                              2
      shape.draw(str => println(s"ProcessMessage: $str"))
      Response(s"ProcessMessage: $shape drawn")
    case Response(unexpected) =>                                     3
      val response = Response(s"ERROR: Unexpected Response: $unexpected")
      println(s"ProcessMessage: $response")
      response
1

We’re done, so print a message that we’re exiting and return Exit to the caller.

2

Call draw on shape, passing it an anonymous function that knows what to do with the string generated by draw. In this case, it just prints the string to the console and sends a Response to the caller.

3

ProcessMessages doesn’t expect to receive a Response message from the caller, so it treats it as an error. It returns a new Response to the caller.

One of the commonly taught tenets of object-oriented programming is that you should never use case statements that match on instance type, because inheritance hierarchies evolve, which breaks these case statements. Instead, polymorphic functions should be used. So, is the pattern-matching code just discussed an antipattern?

Recall that we defined Shape.draw to call the toString method on the Shape, which is automatically generated by the compiler for each concrete subclass because they are case classes. Hence, the code in the first case statement invokes a polymorphic toString operation and we don’t match on specific subtypes of Shape. This means our code won’t break if a user adds a new shape to the class hierarchy by subclassing Shape, which we encourage.

The case clauses match on subtypes of Message, but we protected ourselves from unexpected change by making Message a sealed hierarchy. If we add a new message type here, we can modify the match expression accordingly.

Hence, we have combined polymorphic dispatch from object-oriented programming with pattern matching, a workhorse of functional programming. This is one way that Scala elegantly integrates these two programming paradigms.

Finally, here is the ProcessShapesDriver that runs the example:

// src/main/scala/progscala3/introscala/shapes/ProcessShapesDriver.scala
package progscala3.introscala.shapes

@main def ProcessShapesDriver =                                      1
  val messages = Seq(                                                2
    Draw(Circle(Point(0.0,0.0), 1.0)),
    Draw(Rectangle(Point(0.0,0.0), 2, 5)),
    Response(s"Say hello to pi: 3.14159"),
    Draw(Triangle(Point(0.0,0.0), Point(2.0,0.0), Point(1.0,2.0))),
    Exit)

  messages.foreach { message =>                                      3
    val response = ProcessMessages(message)
    println(response)
  }
1

An entry point for the application.

2

A sequence of messages to send, including a Response in the middle that will be considered an error in ProcessMessages. The sequence ends with Exit.

3

Iterate through the sequence of messages, call ProcessMessages.apply() with each one, then print the response.

Let’s try it! At the sbt prompt, type run, which will compile the code if necessary and then present you with a list of all the code examples that have a main method:

> run
[info] Compiling ...

Multiple main classes detected, select one to run:

 ...
 [28] progscala3.introscala.shapes.ProcessShapesDriver
 ...

Enter number:

Enter 28 (or whatever number is shown for you) and the following output is written to the console (wrapped to fit):

Enter number: 28

[info] running progscala3.introscala.shapes.ProcessShapesDriver
ProcessMessage: draw: Circle(Point(0.0,0.0),1.0)
Response(ProcessMessage: Circle(Point(0.0,0.0),1.0) drawn)
ProcessMessage: draw: Rectangle(Point(0.0,0.0),2.0,5.0)
Response(ProcessMessage: Rectangle(Point(0.0,0.0),2.0,5.0) drawn)
ProcessMessage: Response(ERROR: Unexpected Response: Say hello to pi: 3.14159)
Response(ERROR: Unexpected Response: Say hello to pi: 3.14159)
ProcessMessage: draw: Triangle(Point(0.0,0.0),Point(2.0,0.0),Point(1.0,2.0))
Response(ProcessMessage: Triangle(Point(0.0,0.0),Point(2.0,0.0),...) drawn)
ProcessMessage: exiting...
Exit
[success] ...

Make sure you understand how each message was processed and where each line of output came from.

Recap and What’s Next

We introduced many of the powerful and concise features of Scala. As you explore Scala, you will find other useful resources that are available on http://scala-lang.org. You will find links for libraries, tutorials, and various papers that describe features of the language.

Next we’ll continue our introduction to Scala features, emphasizing the various concise and efficient ways of getting lots of work done.

1 Lately, the acronym API has been used to refer to the interfaces exposed by services. I’ll use API in its original sense, the types, methods, and values exposed by a code module.

2 For more details on these and other tools, see ScalaTools.

3 In fact, there is a single instance of Unit named (). If you are curious about that name, see SumTypesVsProductTypes.

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

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