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. Thensbt
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.
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 :quitexit
the REPL :type <expression> evaluate thetype
of the given expression :doc <expression> print the documentationfor
the given expression :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. "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.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. 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
@
7673711
e
=
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
object
UpperMain1
:
def
main
(
params
:
Array
[
String
]
)
:
Unit
=
(
"UpperMain1.main: "
)
params
.
map
(
s
=>
s
.
toUpperCase
)
.
foreach
(
s
=>
printf
(
"%s "
,
s
)
)
println
(
""
)
def
main
(
params
:
Array
[
String
]
)
:
Unit
=
(
"main: "
)
params
.
map
(
s
=>
s
.
toUpperCase
)
.
foreach
(
s
=>
printf
(
"%s "
,
s
)
)
println
(
""
)
@main
def
Hello
(
params
:
String
*
)
:
Unit
=
(
"Hello: "
)
params
.
map
(
s
=>
s
.
toUpperCase
)
.
foreach
(
s
=>
printf
(
"%s "
,
s
)
)
println
(
""
)
Declare the package location,
progscala3.introscala
.Declare a
main
method, a program entry point, inside anobject
. I’ll explain what anobject
is shortly.Declare an alternative
main
entry point as a top-level method, outside anyobject
, but scoped to the current package,progscala3.introscala
.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, Array
s 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 java
…progscala3.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 String
s (called repeated parameters), which will be passed to the method body, where params
is implemented with an immutable Seq[String]
. Mutable Array
s 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
)
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. No members are defined, so we omit the colon (
:
) at the end of the class signature.Declare an abstract class for geometric shapes. It needs a colon because it defines a method
draw
.Implement a
draw
method for rendering the shapes. The comment uses the Scaladoc conventions for documenting the method, which are similar to Javadoc conventions.A circle with a center and radius, which subtypes (
extends
)Shape
.A rectangle with a lower-left point, height, and width. To keep it simple, the sides 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 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
)
.
.
.
Here’s our first example where
new
is still needed. Without it, the compiler would think we are callingPoint.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 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
calledMessage
. Atrait
is similar to an abstract base class. (We’ll explore the differences later.) All messages exchanged are subtypes ofMessage
. I explain thesealed
keyword in a moment.A message to draw the enclosed
Shape
.A message with a response to a previous message received from a caller.
Signal termination.
Exit
has no state or behavior of its own, so it is declared acase 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
:
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 only need one instance, so we use an
object
, but it would be easy enough to make this aclass
and instantiate as many as we need for scalability and other needs.Define the
apply
method that takes aMessage
, processes it, then returns a newMessage
.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
=>
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
onshape
, passing it an anonymous function that knows what to do with the string generated bydraw
. In this case, it just prints the string to the console and sends aResponse
to the caller.ProcessMessages
doesn’t expect to receive aResponse
message from the caller, so it treats it as an error. It returns a newResponse
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 Message
s 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
=
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. It takes no arguments, and if you provide arguments when you run this application, they will be ignored.
A sequence of messages to send, including a
Response
in the middle that will be considered an error inProcessMessages
. The sequence ends withExit
.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.