Let’s start with a brief look at why you should investigate Scala. Then we’ll dive in and write some code.
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:
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.
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.
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.
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.
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.
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
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 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.
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.
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.
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.
Coursier (get-coursier.io) is a new dependency resolver and tool manager. It replaces Maven and Ivy, the traditional dependency resolvers for Java and Scala projects. Written in Scala, it is fast and easy to embed in other applications.
Installing Coursier is not required, but it is recommended as it will install all the tools you need and more, in addition to providing many other convenient features. For example, the Coursier CLI is handy for managing dependency metadata and artifacts from Maven and Ivy repositories, although we’ll do this indirectly through the SBT build for the examples. Coursier also has convenient commands for working with applications embedded in libraries and it can even manages installations of different Java JDK versions.
See the installation instructions at get-coursier.io/docs/cli-installation for details. After installing Coursier, see get-coursier.io/docs/cli-install for a description of the coursier install
command for installing other tools, like SBT (sbt-launcher
) and Scala. For example, this command shows the default set of available applications (at the time of this writing in early 2020):
$
unzip -l"
$(
cs fetch io.get-coursier:apps:0.0.8)
"
|
grep json188
02-09-2020 17:48 ammonite.json175
02-09-2020 17:48 coursier.json332
02-09-2020 17:48 cs.json241
02-09-2020 17:48 dotty-repl.json150
02-09-2020 17:48echo
-graalvm.json108
02-09-2020 17:48echo
-java.json172
02-09-2020 17:48echo
-native.json335
02-09-2020 17:48 giter8.json135
02-09-2020 17:48 mdoc.json323
02-09-2020 17:48 mill-interactive.json321
02-09-2020 17:48 mill.json524
02-09-2020 17:48 sbt-launcher.json222
02-09-2020 17:48 scala.json209
02-09-2020 17:48 scalac.json213
02-09-2020 17:48 scaladoc.json180
02-09-2020 17:48 scalafix.json184
02-09-2020 17:48 scalafmt.json204
02-09-2020 17:48 scalap.json
(Many of the tools shown here are discussed in ScalaTools
.) For now, run the following command to install the sbt
command. Note that the .json
suffix is removed from the name. I recommend installing all the scala*
commands, but they aren’t strictly necessary, as discussed below.
coursier install --install-dir path sbt-launcher
Note that the --install-dir path
arguments are optional. Depending on your platform, the default installation location will vary. Whatever location you use, make sure you add it to your PATH
.
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).
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.
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.
Now that you have the tools you need, you can download and build the code examples.
Download the code examples as described in GettingCodeExamples
.
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.
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.
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.
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).
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.
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!”
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 :quitexit
the interpreter :type <expression> evaluate thetype
of the given expression :doc <expression> print the documentationfor
the given expresssion :imports show importhistory
:reset reset the repl to its initial state, ... scala> vals
=
"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.
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
.
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.
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.01
s
+
The
!=
operator
is
implemented
with
the
equals
method
0.001
s
...
[
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
.
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.
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 (<…>
).
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
@
7673711
e
=
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
object
UpperMain1
:
def
main
(
params
:
Array
[
String
]
)
:
Unit
=
params
.
map
(
s
=>
s
.
toUpperCase
(
)
)
.
foreach
(
s
=>
printf
(
"%s "
,
s
)
)
println
(
""
)
end
main
@main
def
hello
(
params
:
String
*
)
=
main
(
params
.
toArray
)
end
UpperMain1
Declare the package location.
Declare a main
method, the program entry point.
For long methods (unlike this one), you can use end name
, but this is optional.
An alternative way to define an entry point method.
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 static
s 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.
Why doesn’t Scala support static
s? 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.)
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
)
abstract
class
Shape
(
)
:
/**
*
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
"
)
case
class
Circle
(
center
:
Point
,
radius
:
Double
)
extends
Shape
case
class
Rectangle
(
lowerLeft
:
Point
,
height
:
Double
,
width
:
Double
)
extends
Shape
case
class
Triangle
(
point1
:
Point
,
point2
:
Point
,
point3
:
Point
)
extends
Shape
Declare a class for two-dimensional points.
Declare an abstract class for geometric shapes.
Implement a draw
method for “rendering” the shapes. The documentation comment uses the same conventions that Java uses.
A circle with a center and radius.
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.
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)
.
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
.
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).
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.
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.
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”.
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 Message
s that can be exchanged between modules.
// src/main/scala/progscala3/introscala/shapes/Messages.scala
package
progscala3.introscala.shapes
sealed
trait
Message
case
class
Draw
(
shape
:
Shape
)
extends
Message
case
class
Response
(
message
:
String
)
extends
Message
case
object
Exit
extends
Message
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.
A message to draw the enclosed Shape
.
Response
is used to return an arbitrary string message to a caller in response to a message received from the caller.
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
:
def
apply
(
message
:
Message
)
:
Message
=
message
match
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
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.
Define the apply
method that takes a Message
, processes it, then returns a new Message
.
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
=>
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
We’re done, so print a message that we’re exiting and return Exit
to the caller.
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.
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
=
val
messages
=
Seq
(
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
=>
val
response
=
ProcessMessages
(
message
)
println
(
response
)
}
An entry point for the application.
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
.
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.
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
.
3.145.201.17