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 tools to large sophisticated applications. So let’s consider each of those ideas in more detail:

A Java Virtual Machine (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 compiles Scala to native machine code, bypassing the JVM and JavaScript runtimes.

Object-oriented programming

Scala fully supports object-oriented programming (OOP). Scala traits provide a clean way to implement code using mixin composition. Scala provides convenient and familiar OOP consistently for all types, even numeric types, while still enabling highly performant code generation.

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, code without side effects, and functional collections all contribute to concise, powerful, and correct code.

A sophisticated type system with static typing

Scala’s rich, static type system goes a long way toward bug-free code where mistakes are caught at compile time. 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 other languages become concise idioms in Scala. Scala provides several facilities for building domain-specific languages (DSLs), APIs that feel native to users.

Scalable architectures

You can write tiny, single-file tools 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 20, 2004. Martin is a professor in the School of Computer and Communication Sciences at the École 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 recruited by Sun Microsystems to produce the reference implementation of javac with generics, the ancestor of the Java compiler that ships with the Java Developer Kit (JDK) today.

The Appeal of Scala

The growth of Scala users since it was introduced over 15 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 productivity and success.

I believe Scala is a language for professional developers. Not all Scala users are professionals, of course, but Scala is the kind of language a professional in our field needs, rich in features, highly performant, and 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 removes unimportant constructs to make the language smaller and more regular. Also, the previous experimental approach to macros is replaced with a new, principled approach to metaprogramming.

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 standard library as Scala 2.13, eliminating a class of changes you would otherwise have to make to your code when upgrading. Hence, if necessary, I recommend upgrading to Scala 2.13 first to update your use of the standard library as needed, then upgrade to Scala 3.

In addition, to make the transition to breaking language changes as painless as possible, there are several ways to compile Scala 3 code that allows or disallows deprecated Scala 2 constructs. There are even compiler flags that will do some code rewrites automatically for you! See “Scala 3 Versions” and “The scalac Command-Line Tool” in Chapter 22 for details.

For a complete guide to migrating to Scala 3, see the Scala Center’s Scala 3 Migration Guide.

Installing the Scala Tools You Need

There are many options for installing tools and building Scala projects. See Chapter 22 and the Scala website’s Getting Started for more details on available tools and options for starting with Scala. Here, I’ll focus on the simplest way to install the tools needed for the book’s example code.

The examples target Scala 3 for the JVM. See the Scala.js and Scala Native websites for information on targeting those platforms.

You only need to install two tools:

  • A recent Java JDK, version 8 or newer. Newer long-term versions are recommended, like versions 11 or 15 (the latest release at the time of this writing).

  • sbt, the de facto build tool for Scala.

Follow the instructions for installing the JDK and sbt on their respective websites.

When we use the sbt command in “Using sbt”, it will bootstrap everything else needed, including the scalac compiler and the scala tool for running code.

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 “Getting the Code Examples”.

Start sbt

Open a terminal and change to the root directory for the code examples. Type the command sbt test to download all the library dependencies you need, including the Scala compiler. This will take a while. 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 automatically downloads the Scala library and tools we need, including the required third-party dependencies.

More Tips

In your browser, it’s useful to bookmark the Scala standard library’s Scaladoc documentation. For your convenience, when I mention a type in the library, I’ll often include a link to the corresponding Scaladoc 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 a “Source” link.

Any text editor or integrated development environment (IDE) will suffice for working with the examples. Scala plug-ins exist for all the popular editors and IDEs, such as IntelliJ IDEA and Visual Studio Code. Once you install the required Scala plug-in, most environments can open your sbt project, automatically importing all the information they need, like the Scala version and library dependencies.

Support for Scala in many IDEs and text editors is now based on the Language Server Protocol (LSP), an open standard started by Microsoft. The Metals project implements LSP for Scala. The Metals website has installation details for your particular IDE or editor. In general, the community for your favorite editor or IDE is your best source of up-to-date information on Scala support.

Tip

If you like working with Scala worksheets, many of the code examples can be converted to worksheets. See the code examples README for details.

Using sbt

Let’s cover the basics of using sbt, which we’ll use to build and work with the code examples.

When you start sbt, if you don’t specify a task to run, sbt starts an interactive shell. 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, zsh, or the Window’s command shell), where you start the sbt command, the > is the default sbt interactive prompt, and the # starts a comment. You can type most of these commands in any order:

$ sbt
> help      # Describe commands.
> tasks     # Show the most commonly used, available tasks.
> tasks -V  # 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 interactive Scala environment.
> run       # Run one of the "main" methods (applications) in the project.
> show x    # Show the value of setting or task "x".
> exit      # Quit the sbt shell (also control-d works).

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

sbt:programming-scala-3rd-ed-code-examples>

To save space, I’ll use the more concise prompt, >, when showing sbt sessions.

Tip

A handy sbt technique is to add a tilde, ~, 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 my code and running my tests. Since, sbt uses an incremental compiler, you don’t have to wait for a full rebuild every time. Break out of these loops by hitting Return.

The scala CLI command has a built-in REPL (read, eval, print, loop). This is a historical term, going back to LISP. It’s more accurate than interpreter, which is sometimes used. Scala code isn’t interpreted. It is always compiled and then run, even when using the interactive REPL where bits of code at a time are entered and executed. Hence, I’ll use the term REPL when referring to this use of the scala CLI. You can invoke it using the console command in sbt. We’ll do this a lot to work with the book’s code examples. The Scala REPL prompt is scala>. When you see that prompt in code examples, I’m using the REPL.

Before starting the REPL, sbt console will build your project and set up the classpath with your compiled artifacts and dependent libraries. This convenience means it’s rare to use the scala REPL outside of sbt because you would have to set up the classpath yourself.

To exit the sbt shell, use exit or Ctrl-D. To exit the Scala REPL, use :quit or Ctrl-D.

Tip

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

I configured the compiler options for the code examples (in build.sbt) to pass -source:future. This flag causes warnings to be emitted for constructs that are still allowed in Scala 3.0, but it will be removed in Scala 3.1 or deprecated with planned removal in a subsequent release. I’ll cite specific examples of planned transitions as we encounter them. There are several language versions that can be used with the -source option. See “Scala 3 Versions” for details), especially when starting your own code migrations to Scala 3.

Note

Because I’m using the “aggressive” -source:future option, you’ll see warnings when using sbt console that won’t appear in other Scala 3 projects that don’t use this setting.

Running the Scala Command-Line Tools Using sbt

When the Scala 3 command-line tools are installed separately (see “Command-Line Interface Tools” for details), the Scala compiler is called scalac and the REPL is called scala. We will let sbt run them for us, although I’ll show you how to run them directly as well.

Let’s run a simple Scala program. Consider this “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()
val uppers = up.convert(List("Hello", "World!"))
println(uppers)
Tip

Most listings, like this one, start with a comment that contains the file path in the code examples, so it’s easy for you to locate the file. Not all examples have files, but if you see a listing with no path comment, it often continues where the previous listing left off.

I’ll explain the details of this code shortly, but let’s focus now on running it.

Change your current working directory to the root of the code examples. Start sbt and run the console task. Then, use the :load command to compile and run the contents of the file. In the next listing, the $ is the terminal’s prompt, > is the sbt prompt, scala> is the Scala REPL’s prompt, and the ellipses (…) are for suppressed output:

$ sbt
...
> console
...
scala> :load 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!”

All the code examples that we’ll use in the REPL will have paths that start with src/script. However, in most cases you can copy and paste code from any of the source files to the REPL prompt.

If you have the scala REPL for Scala installed separately, you can enter scala at the terminal prompt, instead of the separate sbt and console steps. However, most of the example scripts won’t run with scala outside of sbt because sbt console includes the libraries and compiled code in the classpath, which most of the scripts need.1

Here is a more complete REPL session to give you a sense of what you can do. Here I’ll combine sbt and console into one step (some output elided):

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

:help                    print this summary
:load <path>             interpret lines in a file
:quit                    exit the REPL
:type <expression>       evaluate the type of the given expression
:doc <expression>        print the documentation for the given expression
: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. "Control-D" also works.

The list of commands available and the output of :help may change between Scala releases.

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

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.

The type of something is given by its name, a colon, and then the type. We didn’t explicitly specify any type information here because the types could be inferred. When you provide types explicitly or when they are inferred and shown for you, they are called type declarations.2 The output of the REPL shows several examples.

When a type is added to a declaration, the syntax name: String, for example, is used instead of String name. The latter would be more ambiguous in Scala because of type inference, where type information can be omitted from the code yet inferred by the compiler.

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.

See “Command-Line Interface Tools” for more information about the command-line tools, such as using the scala CLI to run compiled code outside of sbt.

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 I introduce a type in the Scala library, find its entry in the Scaladoc. Scala 3 uses the Scala 2.13 library with a few minor additions.

Scala follows common comment conventions. A // comment goes to the end of a line, while a /* comment */ can cross line boundaries. Comments intended to be included in Scaladoc documentation use /** comment */.

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. Note that sbt follows Apache Maven conventions that directories for compiled source code go under src/main/scala and tests go under src/test/scala. After that is the package definition, progscala3.objectsystem.equality, corresponding to file path progscala3/objectsystem/equality. Packages organize code hierarchically. The test inside of the file is defined as a class named EqualitySuite.

Note

Scala packages, names, and file organization mostly follow Java conventions. Java requires that the directory path and filename must match the declared package and a single public class within the file. Scala doesn’t require conformance to these rules, but it is conventional to follow them, especially for larger code bases. The code examples follow these conventions.

Finally, many of the files under src/main/scala define entry points (such as main methods), the starting points for running those small applications. You can execute them in one of several ways.

First, use sbt’s run command. It will find all the entry points and prompt you to pick which one. Note that sbt will only search src/main/scala and src/main/java. When you compile and run tests, src/test/scala and src/test/java are searched. The src/script is ignored by sbt.

Let’s use another example we’ll study later in the chapter, src/main/scala/progscala3/introscala/UpperMain2.scala. Invoke run hello world, where run is the sbt task and hello world are arbitrary arguments that will be passed to a program we’ll choose from the list that is printed for us (over 50 choices!). Enter the number shown for progscala3.introscala.Hello2:

> run hello world
...

Multiple main classes detected, select one to run:

 ...
 [38] progscala3.introscala.Hello2
 ...

38    <--- What you type!

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

This program converts the input arguments to uppercase and prints them.

A second way to run this program is to use runMain and specify the same fully qualified path to the main class that was shown, progscala3.introscala.Hello2. This skips the prompt:

> runMain progscala3.introscala.Hello2 hello world again!
...
[info] running progscala3.introscala.Hello2
HELLO WORLD AGAIN!
[success] Total time: 0 s, completed Feb 29, 2020, 5:18:05 PM
>

This code is already compiled, so you can also run it outside of sbt with the scala command. Now the correct classpath must be provided, including all dependencies. This example is easy; the classpath only needs the output root directory for all of the compiled .class files. I’m using a shell variable here to fit the line in the space; change the 3.0.0 to match the actual version of Scala used:3

$ cp="target/scala-3.0.0/classes/"
$ scala -classpath $cp progscala3.introscala.Hello2 Hello Scala World!
HELLO SCALA WORLD!

There’s one final alternative we can use. As we’ll see shortly, UpperMain2.scala defines a single entry point. Because of this, the scala command can actually load the source file directly, compile, and run it in one step, without a scalac step first. We won’t need the -classpath argument now, but we will need to specify the file instead of the fully qualified name used previously:

$ scala src/main/scala/progscala3/introscala/UpperMain2.scala Hello World!
HELLO WORLD!

Let’s explore the implementations of these examples. First, here is Upper1.scala 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()
val uppers = up.convert(List("Hello", "World!"))
println(uppers)

We declare a class, Upper1, using the class keyword, followed by a colon (:). The entire class body is indented on the next two lines.

Note

If you know Scala already, you might ask why are there no curly braces {} and why is a colon (:) after the name Upper1? I’m using the new optional braces syntax that I’ll discuss in more depth in “New Scala 3 Syntax—Optional Braces”.

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 for the reader and also avoids occasional surprises from the type inference process.

Note

I’ll use parameters to refer to the list of things a method expects to be passed when you call it. I’ll use arguments to refer to values you actually pass to it when making the call.

Type definitions are specified using name: type syntax. The parameter list is strings: Seq[String] and the return type of the method is Seq[String], after the parameter list.

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

One reason is to reduce ambiguity. If you omit the return type, Scala can infer it. 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 convert 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 kind of sequence returned by this method will be the same kind that was passed into it as an argument, like Vector or List (both of which are immutable collections).

Collection types like Seq[T] are parameterized types, where T is the type of the elements in the sequence. Scala uses square brackets ([]) for parameterized types, whereas several other languages use angle brackets (<>).

List[T] is an immutable linked list. Accessing the head of a List is O(1), while accessing an arbitrary element at position N is O(N). Vector[T] is a subtype of Seq[T] with almost O(1) for all access patterns.

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. To avoid ambiguity, Scala reserves square brackets for parameterized types so that characters like < and > can be used as identifiers.

Inside the body of convert, we use the map method to iterate over the elements and apply a transformation to each one and then construct a new collection with the results.

The function passed to the map method to do the transformation is an unnamed (anonymous) function literal of the form (parameters) => body:

(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 body calls the toUpperCase method on s.4 The result of this call is automatically returned by the function literal. In Scala, the last expression in a function, method, or other block 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. It is only used for early returns in the middle of methods.)

On the JVM, functions are implemented using JVM lambdas, as the REPL will indicate to you:

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

Note that the REPL treats this function like any other value and gives it a synthesized name, res0, when you don’t provide one yourself (e.g., val f = (s: String) => s.toUpperCase). Read-only values are declared using the val keyword.

Back to Upper1.scala, the last two lines, which are outside the class definition, create an instance of Upper1 named up, using new Upper1(). Then up is used to convert two strings to uppercase. Finally, the resulting sequence uppers is printed with println. Normally, println expects a single string argument, but if you pass it an object, like a Seq, the toString method will be called. If you run sbt console, then copy and paste the contents of the Upper1.scala file, the REPL will tell you that the actual type of the Seq[String] is List[String] (a linked list).

So src/script/…/Upper1.scala is intended for copying and pasting (or using :load) in the REPL. Let’s look at another implementation that is compiled and then run. I added Main to the source filename. 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
    print("UpperMain1.main: ")
    params.map(s => s.toUpperCase).foreach(s => printf("%s ",s))
    println("")

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

@main def Hello(params: String*): Unit =                        4
  print("Hello: ")
  params.map(s => s.toUpperCase).foreach(s => printf("%s ",s))
  println("")
1

Declare the package location, progscala3.introscala.

2

Declare a main method, a program entry point, inside an object. I’ll explain what an object is shortly.

3

Declare an alternative main entry point as a top-level method, outside any object, but scoped to the current package, progscala3.introscala.

4

Declare an entry point method where we can use a different name and we have more flexible options for the argument list.

Packages work much like they do in other languages. They provide a namespace for scoping declarations and access to them. Here, the declarations exist in the progscala3.introscala package.

You have probably seen classes in other languages that encapsulate members, meaning methods, fields (or attributes) that hold state, and so forth. In many languages, the entry point where the program starts is a main method. In Java, this method is defined inside a class and declared static, meaning it is not tied to any one instance. You can reference any static definition with the syntax UpperMain1.main, to use our example.

The pattern of static declarations in classes is so pervasive that Scala builds it into the language. Instead, we declare an object UpperMain1, using the object keyword. Then we declare main and other members using the same syntax we would use in classes. There is no static keyword in Scala.

This file has three entry points. The first one, UpperMain1.main, is how you declare entry points in Scala 2. Following Java conventions, the name main is required and it is declared with an Array[String] parameter for the user-specified arguments, even if the program takes no arguments or takes specific arguments in a specific order, like an integer followed by two strings. You have to handle parsing the arguments. Also, Arrays in Scala are mutable, which can be a source of bugs. Using immutable arguments is inherently safer. All these issues are addressed in the last entry point, Hello, as we’ll discuss in a moment.

Inside UpperMain1.main, we print the name of the method first (without a newline), which will be useful for contrasting how these three entry points are invoked. Then we map over the input arguments (params), converting them to uppercase and returning a new collection. Finally, we use another collections method called foreach to iterate over the new collection and print each string using printf, which expects a formatting string and arguments, s here, to compose the final string.5

Let’s run with UpperMain1.main:

> runMain progscala3.introscala.UpperMain1 Hello World!
UpperMain1.main: HELLO WORLD!
>

The method main itself is not part of the qualified path, just the enclosing object UpperMain1.

Scala 3 introduces two new features for greater flexibility. First, you can declare methods, variables, etc., outside objects and classes. This is how the second main method is declared, but otherwise it works like UpperMain1.main. It is scoped differently, as we can see when we use it:

> runMain progscala3.introscala.UpperMain1$package Hello World!
main: HELLO WORLD!
>

Note how the definition is scoped to the package plus source filename! If you rename the file to something like FooBar.scala and recompile, then the command becomes runMain progscala3.introscala.FooBar$package…. Adding the source file to the scope avoids collisions with other definitions in the same package scope, but with different source files. However, having $package in the name is inconvenient for Linux and macOS shells like bash, so I don’t recommend defining an entry point this way.

Instead, I recommend the second, new Scala 3 feature, an alternative way of defining entry points, which is shown by our third entry point, the Hello method. The @main annotation marks this method as an entry point. Note how we refer to it when we run it:

> runMain progscala3.introscala.Hello Hello World!
Hello: HELLO WORLD!
>

Now the method name is used. Normally you don’t name methods starting with an uppercase letter, but it’s useful for entry points if you want invocation commands to look similar to Java invocations like javaprogscala3.introscala.Hello…. Hello, which is also declared outside an object, but this isn’t required.

The new @main entry points have several advantages. They reduce boilerplate when defining them. They can be defined with parameter lists that match the expected arguments, such as sequences, strings, and integers. Here, we want zero or more string arguments. The * in params: String* means zero or more Strings (called repeated parameters), which will be passed to the method body, where params is implemented with an immutable Seq[String]. Mutable Arrays are avoided.

Note that the type of the return value of all three methods is Unit. For now, think of Unit as analogous to void in other languages, meaning nothing useful is returned.

Note

Because there are three entry points defined in this file, we can’t use scala to parse and run this file in one step. That’s why I used UpperMain2 earlier, instead. We’ll explore that file shortly, where we’ll see it has one and only one entry point.

Declaring UpperMain1 as an object makes it a singleton, meaning there will always be only one instance of it, which the Scala runtime will create for us. 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 other class declarations, but they are used when you need one and only one instance to hold some methods and fields, as opposed to the situation where you need multiple instances, each with fields of unique values per instance and methods that operate on a single instance at a time.

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 limits scalability options. However, we’ll see plenty of examples in the book where objects are used effectively.

Tip

To avoid confusion, I’ll use instance, rather than object, when I refer to an instance created from a class with new or the single instance of an object. Because classes and objects are so similar, I’ll use type generically for them. All the types we’ll see, like String, are implemented as classes or objects.

Returning to the implementation details, note the function we passed to map:

s => s.toUpperCase

Our previous example used (s: String) => s.toUpper(s). 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. So the type declaration String isn’t needed.

The foreach method is used when we want to process each element and perform only side effects, without returning a new value. Here we print a string to standard output (without a newline after each one). In contrast, map returns a new value for each element (and side effects should be avoided). The last println call prints a newline before the program exits.

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

Look again at the second line inside each method, how concise it is where we compose operations together. Sequencing transformations 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 operate much like similar constructs in other languages. Scala automatically imports many commonly used types and object members, like Seq, List, Vector, and the print* methods we used, which are actually methods in an object called scala.Console. Most of these things that are automatically imported are defined in a library object called Predef.

For completeness, let’s discuss how to compile and run the example outside sbt. First you use scalac to compile to a JVM-compatible .class file. Often, multiple class files are generated. Then you use scala to run it.

If you installed the Scala command-line tools separately (see “Command-Line Interface Tools” for details), run the following two commands (ignoring the $ shell prompt) in a terminal window at the root of the project:

$ scalac src/main/scala/progscala3/introscala/UpperMain1.scala
$ scala -classpath . progscala3.introscala.Hello Hello compiled World!
Hello: HELLO COMPILED WORLD!

You should now have new directories progscala3/introscala with several .class and .tasty files, including a file named UpperMain1.class. Class files are processed by the JVM, and tasty files are an intermediate representation used by the compiler. Scala must generate valid JVM byte code and files. For example, the directory structure must match the package structure. The -classpath . option adds the current directory to the search classpath, although . is the default.

Allowing sbt to compile it for us instead, we need a different -classpath argument to reflect the directory where sbt writes class files:

$ scala -classpath target/scala-3.0.0/classes progscala3.introscala.Hello Bye!
BYE!

Let’s do one last version to see a few other useful ways of working with collections for this scenario. This is the version we ran previously:

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

@main def Hello2(params: String*): Unit =
  val output = params.map(_.toUpperCase).mkString(" ")
  println(output)

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

Note the function passed to map. The following function literals are essentially the same:

s => s.toUpperCase
_.toUpperCase

Rather than providing a name for the single argument, we can use _ as a placeholder. This generalizes to functions with two or more arguments, where each use of _ takes the place of one argument. This means that placeholders can’t be used if it’s necessary to refer to any one of the arguments more than once.

As before, we can run this code with sbt using runMain progscala3.introscala.Hello2… We also saw previously that we can use the scala command to compile and run it in one step because it has a single entry point:

$ scala src/main/scala/progscala3/introscala/UpperMain2.scala last Hello World!
LAST HELLO WORLD!

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.

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. No members are defined, so we omit the colon (:) at the end of the class signature.

2

Declare an abstract class for geometric shapes. It needs a colon because it defines a method draw.

3

Implement a draw method for rendering the shapes. The comment uses the Scaladoc conventions for documenting the method, which are similar to Javadoc conventions.

4

A circle with a center and radius, which subtypes (extends) Shape.

5

A rectangle with a lower-left point, height, and width. To keep it simple, the sides 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 body of a class or object is the constructor, so you list the parameters for the constructor after the class name and before the class body.

The case keyword before the class declaration causes special handling. First, each constructor parameter is automatically converted to a read-only (immutable) field for Point instances. In other words, it’s as if we put val before each field declaration. When 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 write point.y = 3.0 causes a compilation error.

You can also provide default values for constructor and method parameters. 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. This implies that when you define a default value for one parameter, you must also do this for all parameters to its right.

Finally, case-class instances are constructed without using new, such as val p = Point(). Scala 3 adds the ability to omit new when constructing instances for most noncase classes too. We used new Upper1() previously, but omitting new would also work. We’ll do that from now on, but there are situations we’ll see where new is still necessary.

Let’s use sbt console to play with these types. I recommend you do this with most of the book’s examples. Recall that scala> is the scala REPL prompt. When you see a line starting with // src/script/, it’s not part of the session, but it shows you where you can find this code in the examples distribution.

$ sbt
> console
...
// src/script/scala/progscala3/introscala/TryShapes.scala

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

Like many other languages, import statements use the * character as a wildcard to import everything in the progscala3.introscala.shapes package. This is a change from Scala 2, where _ was used as the wildcard. However, it is still allowed for backward compatibility, until a future release of Scala 3. Recall that we also saw _ used in function literals as an anonymous placeholder for a parameter, instead of using an explicit name.

In the definition of p00, no arguments are specified, so Scala uses 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 uses the default value for the remaining argument, as shown for p20 and p20b. We can even specify the arguments by name. The definition of p02 uses the default value for x but specifies the value for y, using Point(y = 2.0).

Tip

I use named arguments like this a lot, even when it isn’t required, because Point(x = 0.0, y = 2.0) makes my code much easier to read and understand.

While there is no class body for Point, another feature of the case keyword is that the compiler automatically generates several methods for us, including commonly used toString, equals, and hashCode methods. 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, which compares the instances for equality by comparing the fields. (In some languages, == just compares references. Do p20 and p20b point to the same spot in memory?)

The last feature of case classes that we’ll mention now is that the compiler also generates a companion object, a singleton object 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. When I said earlier that it is unnecessary to use new to create instances of case classes like Point, this works because the companion method Point.apply() gets called.

This is true for any instance, either a declared object or an instance of a class, not just for case-class companion objects. 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)         // Same!

It’s a compilation error if no apply method exists for the instance, or the provided argument list is incompatible with what apply expects.6

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. The companion object generated is equivalent to this:

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

Here’s our first example where new is still needed. Without it, the compiler would think we are calling Point.apply again on the righthand side, creating an infinite recursion!

You can add methods to the companion object, including overloaded apply methods. Just declare object Point: explicitly and add the methods. The default apply method will still be generated, unless you define it explicitly yourself.

A more sophisticated apply method might instantiate a different subtype 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.

We can also define an instance apply method in any class. It has whatever meaning we decide 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 provided arguments. Hence, 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.

Continuing with the example, 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 previously. It is a real type, but it behaves roughly like void in other languages.

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 FP, 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 where a function is passed as an argument, just like we might pass instances of Strings, Points, etc. We can also return functions from methods and from other functions. Finally, we can assign functions to variables. This means that functions are first class in Scala because they can be used just like strings and other instances. 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, which 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 called for us. So we can use just ${this}. However, now we’re just referring to a variable, not a longer expression, so we can drop the curly braces and just write $this. Hence, the interpolated string 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.

Continuing with the example, Circle, Rectangle, and Triangle are concrete subtypes (also called subclasses) of Shape. They have no class bodies because Shape and the methods generated for case classes define 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 in a real application, f could 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 is similar to an abstract base class. (We’ll explore the differences later.) All messages exchanged are subtypes of Message. I explain the sealed keyword in a moment.

2

A message to draw the enclosed Shape.

3

A message with a response to a previous message received from a caller.

4

Signal termination. 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 subtypes of Message in the same file. This prevents bugs where users define their own Message subtypes that would break the code we’re about to see in the next file! These are all the allowed messages, known in advance.

Recall that Shape was not declared sealed earlier because we intend for people to create their own subtypes of it. There could be an infinite number of Shape subtypes, in principle. So, use sealed hierarchies when all the possible variants are fixed.

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

We only need one instance, so we use an object, but it would be easy enough to make this a class and instantiate as many as we need for scalability and other needs.

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

The apply method introduces a powerful feature call: match expressions with pattern matching:

message match
  case Exit =>
    expressions
  case Draw(shape) =>
    expressions
  case Response(unexpected) =>
    expressions

The whole message match:… is an expression, meaning it will return a value, a new Message for us to return to the caller. A match expression consists only of case clauses, which do pattern matching on the message passed into the function, followed by expressions to invoke for a match.

The match expressions work a lot like if/else expressions but are more powerful and concise. When one of the patterns matches, the block of expressions after the arrow (=>) is evaluated, up to the next case keyword or the end of the whole expression. 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 and warn you that the case clauses are not exhaustive, meaning they don’t handle all possible inputs. Note that our sealed hierarchy of messages is crucial here. If a user could create 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, so named 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 tenets of OOP is that you should never use if or match statements that match on instance type because inheritance hierarchies evolve. When a new subtype is introduced without also fixing these statements, they break. Instead, polymorphic methods should be used. So, is the pattern-matching code just discussed an antipattern?

Our match expression only knows about Shape and draw. 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 hierarchy.

In contrast, the case clauses match on specific subtypes of Message, but we protected ourselves from unexpected change by making Message a sealed hierarchy. We know by design all the possible Messages exchanged.

Hence, we have combined polymorphic dispatch from OOP with pattern matching, a workhorse of FP. 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. It takes no arguments, and if you provide arguments when you run this application, they will be ignored.

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. Some output elided:

> runMain progscala3.introscala.shapes.ProcessShapesDriver
[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), ...) 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. 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 If you are unfamiliar with the JVM ecosystem, the classpath is a list of locations to search for compiled code, like libraries.

2 Sometimes the type information is called an annotation, but this is potentially confusing with another concept of annotations that we’ll see, so I’ll avoid using this term for types. Type ascriptions is another term.

3 I will periodically update the code examples as new Scala releases come out. The version will be set in the file build.sbt, the scalaVersion string. The other way to tell is to just look at the contents of the target directory.

4 This method takes no arguments, so parentheses can be omitted.

5 This printf-style formatting is so common in programming languages, I’ll assume it needs no further explanation. If it’s new to you, see the link in the paragraph for details.

6 The name apply originated from early theoretical work on computation, specifically the idea of function application.

Get Programming Scala, 3rd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.