This chapter continues our tour of Scala features that promote succinct, flexible code. We’ll discuss organization of files and packages, importing other types, variable and method declarations, a few particularly useful types, and miscellaneous syntax conventions.
If you have prior Scala experience, Scala 3 introduces a new optional braces syntax that makes it look a lot more like Python or Haskell, where Java-style curly braces ({…}
) are replaced with indentation that becomes significant. The examples in the previous chapter and throughout the book use it.
This syntax has several benefits. It is more concise. For Python developers who decide to learn Scala, it will feel more familiar to them (and vice versa).
There is also a new syntax for control structures like for
loops and if
expressions. For example, if condition then …
instead of the older if (condition) …
. Also, for … do println(…)
instead of for {…} println(…)
.
The disadvantage of these changes is that they are strictly not necessary. Some breaking changes in Scala 3 are necessary to move the language forward, but you could argue these syntax changes aren’t required. You also have to be careful to use tabs and spaces consistently for indentation.
The new constructs are the defaults supported by the compiler, but using the compiler flags -old-syntax
and -noindent
will enforce the old syntax constructs. Another flag, -new-syntax
makes the new keywords then
and do
required. Finally, the compiler can now rewrite your code to use whichever style you prefer. Add the -rewrite
compiler flag, for example -rewrite -new-syntax
.
I opposed these changes initially, because they aren’t strictly necessary. However, now that I have worked with them, I believe the advantages outweigh the disadvantages. I also suspect that the syntax changes are the future for Scala and eventually the old syntax could be deprecated. We’ll see. Hence, I have chosen to use the new conventions throughout this edition of the book. I will mention other pros and cons of these changes as we explore examples.
OldVsNewSyntax
provides examples of the old and new syntax.
Even if you continue using the brace syntax, the compiler now requires indentation to use tabs and spaces consistently, unless you use the -noindent
compiler flag.
Semicolons are expression delimiters and they are inferred. Scala treats the end of a line as the end of an expression, except when it can infer that the expression continues to the next line, even for the following example:
scala
>
val
s
=
"hello"
|
+
"world"
|
+
"!"
val
s
:
String
=
helloworld
!
The Scala 2 REPL was more aggressive at interpreting the end of a line as the end of an expression, so the previous example would infer “hello” for the definition of s
and then throw an error for the next two lines.
When using the Scala 2 REPL, use the :paste
mode when multiple lines need to be parsed as a whole. Enter :paste
, followed by the code you want to enter, then finish with Ctrl-D.
Conversely, you can put multiple expressions on the same line, separated by semicolons.
Scala allows you to decide whether a variable is immutable (read-only) or not (read-write) when you declare it. We’ve already seen that an immutable “variable” is declared with the keyword val
:
val
array
:
Array
[
String
]
=
new
Array
(
5
)
Scala is like Java in that most variables are actually references to heap-allocated objects. Hence, the array
reference cannot be changed to point to a different Array
, but the array elements themselves are mutable, so the elements can be modified:
scala
>
val
array
:
Array
[
String
]
=
new
Array
(
5
)
val
array
:
Array
[
String
]
=
Array
(
null
,
null
,
null
,
null
,
null
)
scala
>
array
=
new
Array
(
2
)
1
|
array
=
new
Array
(
2
)
|^^^^^^^^^^^^^^^^^^^^
|
Reassignment
to
val
array
scala
>
array
(
0
)
=
"Hello"
scala
>
array
val
res1
:
Array
[
String
]
=
Array
(
Hello
,
null
,
null
,
null
,
null
)
A val
must be initialized when it is declared, except in certain contexts like abstract fields in type declarations.
Similarly, a mutable variable is declared with the keyword var
and it must also be initialized immediately (in most cases), even though it can be changed later:
scala
>
var
stockPrice
:
Double
=
100.0
var
stockPrice
:
Double
=
100.0
scala
>
stockPrice
=
200.0
stockPrice
:
Double
=
200.0
To be clear, we changed the value of stockPrice
itself. However, the “object” that stockPrice
refers to can’t be changed, because Doubles
in Scala are immutable.
In Java, so-called primitive types, char
, byte
, short
, int
, long
, float
, double
, and boolean
, are fundamentally different than reference objects. Indeed, there is no object and no reference, just the “raw” value. Scala tries to be consistently object-oriented, so these types are actually objects with methods, like reference types (see ReferenceVsValueTypes
). However, Scala compiles to primitives where possible, giving you the performance benefit they provide (see SpecializationForValueTypes
for details).
Consider the following REPL session, where we define a Person
class with immutable first and last names, but a mutable age (because people age, I guess). The parameters are declared with val
and var
, respectively, making them both fields in Person
:
// src/script/scala/progscala3/typelessdomore/Human.scala
scala
>
class
Human
(
val
name
:
String
,
var
age
:
Int
)
// defined class Human
scala
>
val
p
=
new
Human
(
"Dean Wampler"
,
29
)
val
p
:
Human
=
Human
@
165
a128d
scala
>
p
.
name
val
res0
:
String
=
Dean
Wampler
scala
>
p
.
name
=
"Buck Trends"
1
|
p
.
name
=
"Buck Trends"
|^^^^^^^^^^^^^^
|
Reassignment
to
val
name
scala
>
p
.
name
val
res1
:
String
=
Dean
Wampler
scala
>
p
.
age
val
res2
:
Int
=
29
scala
>
p
.
age
=
30
scala
>
p
.
age
val
res3
:
Int
=
30
scala
>
p
.
age
=
31
;
p
.
age
// Use semicolon to join two expressions...
val
res4
:
Int
=
31
The var
and val
keywords only specify whether the reference can be changed to refer to a different object (var
) or not (val
). They don’t specify whether or not the object they reference is mutable.
Use immutable values whenever possible to eliminate a class of bugs caused by mutability. For example, a mutable object is dangerous as a key in hash-based maps. If the object is mutated, the output of the hashCode
method will change, so the corresponding value won’t be found at the original location.
More common is unexpected behavior when an object you are using is being changed by another thread. Borrowing a phrase from Quantum Physics, these bugs are spooky action at a distance. Nothing you are doing locally accounts for the unexpected behavior; it’s coming from somewhere else.
These are the most pernicious bugs in multithreaded programs, where synchronized access to shared, mutable state is required, but difficult to get right. Using immutable values eliminates these issues.
Sometimes we need a sequence of numbers from some start to finish. A Range
literal is just what we need. The following examples show how to create ranges for the types that support them, Int
, Long
, Char
, BigInt
, which represents integers of arbitrary size, and BigDecimal
, which represents floating-point numbers of arbitrary size. Float
and Double
were supported in Scala 2, but floating point arithmetic makes range calculations error prone. Hence, Scala 3 drops ranges for Float
and Double
.
You can create ranges with an inclusive or exclusive upper bound, and you can specify an interval not equal to one (some output elided to fit):
scala
>
1
to
10
// Int range inclusive, interval of 1, (1 to 10)
val
res0
:
scala
.
collection
.
immutable
.
Range
.
Inclusive
=
Range
0
to
10
scala
>
1
until
10
// Int range exclusive, interval of 1, (1 to 9)
val
res1
:
Range
=
Range
0
until
10
scala
>
1
to
10
by
3
// Int range inclusive, every third.
val
res2
:
Range
=
inexact
Range
0
to
10
by
3
scala
>
10
to
1
by
-
3
// Int range inclusive, every third, counting down.
val
res3
:
Range
=
Range
10
to
1
by
-
3
scala
>
1L
to
10L
by
3
// Long
val
res4
:
.
.
.
immutable.NumericRange
[
Long
]
=
NumericRange
1
to
10
by
3
scala
>
'a
'
to
'g
'
by
3
// Char
val
res5
:
.
.
.
immutable.NumericRange
[
Char
]
=
NumericRange
a
to
g
by
scala
>
BigInt
(
1
)
to
BigInt
(
10
)
by
3
val
res6
:
.
.
.
immutable.NumericRange
[
BigInt
]
=
NumericRange
1
to
10
by
3
scala
>
BigDecimal
(
1.1
)
to
BigDecimal
(
10.3
)
by
3.1
val
res7
:
.
.
.
immutable.NumericRange.Inclusive
[
BigDecimal
]
=
NumericRange
1.1
to
10.3
by
3.1
A PartialFunction[A,B]
is a special kind of function with its own literal syntax. A
is the type of the single parameter the function accepts and B
is the return type.
The literal syntax for a PartialFunction
consists only of case
clauses, which we saw in ASampleApplication
, that do pattern matching on the input to the function. There is no function parameter shown explicitly, but when each input is processed, it is passed to the body of the partial function.
For comparison, is a regular function that does pattern matching and similar partial function, adapted from the example we explored in ASampleApplication
:
// src/script/scala/progscala3/typelessdomore/FunctionVsPartialFunction.scala
scala
>
import
progscala3.introscala.shapes._
scala
>
val
func
:
Message
=>
String
=
message
=>
message
match
|
case
Exit
=>
"Got Exit"
|
case
Draw
(
shape
)
=>
s"Got Draw(
$shape
)"
|
case
Response
(
str
)
=>
s"Got Response(
$str
)"
scala
>
val
pfunc
:
PartialFunction
[
Message
,String
]
=
|
case
Exit
=>
"Got Exit"
|
case
Draw
(
shape
)
=>
s"Got Draw(
$shape
)"
|
case
Response
(
str
)
=>
s"Got Response(
$str
)"
scala
>
func
(
Draw
(
Circle
(
Point
(
0.0
,
0.0
),
1.0
)))
|
pfunc
(
Draw
(
Circle
(
Point
(
0.0
,
0.0
),
1.0
)))
|
func
(
Response
(
s"Say hello to pi: 3.14159"
))
|
pfunc
(
Response
(
s"Say hello to pi: 3.14159"
))
val
res0
:
String
=
Got
Draw
(
Circle
(
Point
(
0.0
,
0.0
),
1.0
))
val
res1
:
String
=
Got
Draw
(
Circle
(
Point
(
0.0
,
0.0
),
1.0
))
val
res2
:
String
=
Got
Response
(
Say
hello
to
pi
:
3
.
14159
)
val
res3
:
String
=
Got
Response
(
Say
hello
to
pi
:
3
.
14159
)
Function definitions can be a little harder to read than method definition. The function func
is a named function of type Message => String
. The equal sign starts the body, message => message match ...
.
The partial function, pfunc
, is simpler. It’s type is PartialFunction[Message, String]
. There is no argument list, just a set of case
match clauses, which happen to be identical to the clauses in func
.
The concept of a partial function is simpler than it might appear. In essence, a partial function will only handle certain inputs, so don’t send it something it doesn’t know how to handle. A classic example from mathematics is division, x/y
, which is undefined when the denominator y
is 0. Hence, division is a partial function.
If a partial function is called with an input that doesn’t match one of the case
clauses, a MatchError
is thrown at runtime. Both func
and pfunc
are actually total, because they handle all possible Message
arguments. Try commenting out the case Exit
clauses in both func
and pfunc
. You’ll get a a compiler warning for func
, because it can determine that the match clauses don’t handle all possible inputs. It won’t complain about pfunc
, because that situation is by design.
You can test if a PartialFunction
will match an input using the isDefinedAt
method. This function avoids the risk of throwing a MatchError
exception.
You can also “chain” PartialFunctions
together: pf1.orElse(pf2).orElse(pf3) …
. If pf1
doesn’t match, then pf2
is tried, then pf3
, etc. A MatchError
is only thrown if none of them matches.
Let’s explore these points with the following example:
// src/script/scala/progscala3/typelessdomore/PartialFunctions.scala
val
pfs
:
PartialFunction
[
Any
,
String
]
=
case
s
:
String
=>
"YES"
val
pfd
:
PartialFunction
[
Any
,
String
]
=
{
case
d
:
Double
=>
"YES"
}
val
pfsd
=
pfs
.
orElse
(
pfd
)
A partial function that only matches on strings, using the braceless syntax.
A partial function that only matches on doubles, using braces.
Combine the two functions to construct a new partial function that matches on strings and doubles.
The next block of code in the script tries different values with the three partial functions to confirm expected behavior. Note that integers are not handled by any combination. A helper function tryPF
is used to try the partial function and catch possible MatchError
exceptions. So, a string is returned for both success and failure:
def
tryPF
(
x
:
Any
,
f
:
PartialFunction
[
Any
,String
])
:
String
=
try
f
(
x
)
catch
case
_:
MatchError
=>
"ERROR!"
assert
(
tryPF
(
"str"
,
pfs
)
==
"YES"
)
assert
(
tryPF
(
"str"
,
pfd
)
==
"ERROR!"
)
assert
(
tryPF
(
"str"
,
pfsd
)
==
"YES"
)
assert
(
tryPF
(
3.142
,
pfs
)
==
"ERROR!"
)
assert
(
tryPF
(
3.142
,
pfd
)
==
"YES"
)
assert
(
tryPF
(
3.142
,
pfsd
)
==
"YES"
)
assert
(
tryPF
(
2
,
pfs
)
==
"ERROR!"
)
assert
(
tryPF
(
2
,
pfd
)
==
"ERROR!"
)
assert
(
tryPF
(
2
,
pfsd
)
==
"ERROR!"
)
assert
(
pfs
.
isDefinedAt
(
"str"
)
==
true
)
assert
(
pfd
.
isDefinedAt
(
"str"
)
==
false
)
assert
(
pfsd
.
isDefinedAt
(
"str"
)
==
true
)
assert
(
pfs
.
isDefinedAt
(
3.142
)
==
false
)
assert
(
pfd
.
isDefinedAt
(
3.142
)
==
true
)
assert
(
pfsd
.
isDefinedAt
(
3.142
)
==
true
)
assert
(
pfs
.
isDefinedAt
(
2
)
==
false
)
assert
(
pfd
.
isDefinedAt
(
2
)
==
false
)
assert
(
pfsd
.
isDefinedAt
(
2
)
==
false
)
Finally, we can lift a partial function into a regular (“total”) function that returns an option, Some(value)
, when the partial function is defined for the input argument and None
when it isn’t. We can also unlift a single-parameter function. Here is a session that uses pfs
:
scala
>
val
fs
=
pfs
.
lift
val
fs
:
Any
=>
Option
[
String
]
=
<
function1
>
scala
>
fs
(
"str"
)
val
res0
:
Option
[
String
]
=
Some
(
YES
)
scala
>
fs
(
3.142
)
val
res1
:
Option
[
String
]
=
None
scala
>
val
pfs2
=
fs
.
unlift
val
pfs2
:
PartialFunction
[
Any
,String
]
=
<
function1
>
scala
>
pfs2
(
"str"
)
val
res3
:
String
=
YES
scala
>
tryPF
(
3.142
,
pfs2
)
// Use tryPF we defined above
val
res4
:
String
=
ERROR
!
In the previous example, we combined two partial functions using orElse
. This can be written equivalently in two ways:
val
pfsd1
=
pfs
.
orElse
(
pfd
)
val
pfsd2
=
pfs
orElse
pfd
When a method takes a single parameter, you can drop the period after the object and drop the parentheses around the supplied argument. In this case, pfs orElse pfd
has a cleaner appearance than pfs.orElse(pfd)
, which is why this syntax is popular. This notation is called infix notation, because orElse
is between the object and argument. This syntax is also called operator notation, because it is especially popular when writing libraries where algebraic notation is convenient. For example, you can write your own libraries for matrices and define a method named *
for matrix multiplication using the *
“operator”. Then you can write expressions like val matrix3 = matrix1 * matrix2
.
Scala method names can use most non-alphanumeric characters. When methods are called that take a single parameter, infix operator notation can be used where the period after the object and the parentheses around the supplied argument can be dropped.
Let’s explore method definitions, using a modified version of our Shapes
hierarchy from before.
Here is an updated Point
case class:
// src/main/scala/progscala3/typelessdomore/shapes/Shapes.scala
package
progscala3.typelessdomore.shapes
case
class
Point
(
x
:
Double
=
0.0
,
y
:
Double
=
0.0
)
:
def
shift
(
deltax
:
Double
=
0
.
0
,
deltay
:
Double
=
0.0
)
=
copy
(
x
+
deltax
,
y
+
deltay
)
Define Point
with default initialization values (as before). For case classes, both x
and y
are automatically immutable (val
) fields.
A new shift
method for creating a new Point
instance, offset from the existing Point
. It uses the copy
method that is also created automatically for case classes.
The copy
method allows you to construct new instances of a case class while specifying just the fields that are changing. This is very useful for larger case classes:
scala
>
val
p1
=
new
Point
(
x
=
3.3
,
y
=
4.4
)
// Used named arguments explicitly.
val
p1
:
Point
=
Point
(
3.3
,
4.4
)
scala
>
val
p2
=
p1
.
copy
(
y
=
6.6
)
// Copied with a new y value.
val
p2
:
Point
=
Point
(
3.3
,
6.6
)
Named arguments make client code more readable. They also help avoid bugs when a parameter list has several fields of the same type or it has a lot of parameters. It’s easy to pass values in the wrong order. Of course, it’s better to avoid such parameter lists in the first place.
Next, consider the following changes to Shape.draw()
:
abstract
class
Shape
()
:
def
draw
(
offset:
Point
=
Point
(
0
.
0
,
0
.
0
))(
f
:
String
=>
Unit
)
:
Unit
=
f
(
s"draw(
$offset
,
${
this
}
)"
)
Circle
, Rectangle
, and +Triangle are unchanged and not shown.
Now draw
has two parameter lists, each of which has a single parameter, rather than a single parameter list with two parameters. The first parameter list lets you specify an offset point where the shape will be drawn. It has a default value of Point(0.0, 0.0)
, meaning no offset. The second parameter list is the same as in the original version of draw
, a function that does the drawing.
You can have as many parameter lists as you want, but it’s rare to use more than two.
So, why allow more than one parameter list? Multiple lists promote a very nice block-structure syntax when the last parameter list takes a single function. Here’s how we might invoke this new draw
method to draw a Circle
at an offset:
val
s
=
Circle
(
Point
(
0.0
,
0.0
),
1.0
)
s
.
draw
(
Point
(
1.0
,
2.0
))(
str
=>
println
(
str
))
Scala lets us replace parentheses with curly braces around a supplied argument (like a function literal) for a parameter list that has a single parameter. So, this line can also be written this way:
s
.
draw
(
Point
(
1.0
,
2.0
)){
str
=>
println
(
str
)}
Suppose the function literal is too long for one line or it has multiple statements or expressions? We can rewrite it this way:
s
.
draw
(
Point
(
1.0
,
2.0
))
{
str
=>
println
(
str
)
}
Or equivalently:
s
.
draw
(
Point
(
1.0
,
2.0
))
{
str
=>
println
(
str
)
}
If you use the traditional curly-brace syntax for Scala, it looks like a typical block of code we use with constructs like if
and for
expressions, method bodies, etc. However, the {…}
block is still a function literal we are passing to draw
.
So, this “syntactic sugar” of using {…}
instead of (…)
looks better with longer function literals; they look more like the block structure syntax we know.
Unfortunately, the new optional braces syntax doesn’t work here:
scala
>
s
.
draw
(
Point
(
1.0
,
2.0
))
:
|
str
=>
println
(
str
)
2
|
str
=>
println
(
str
)
|
^
|
parentheses
are
required
around
the
parameter
of
a
lambda
|
This
construct
can
be
rewritten
automatically
under
-
rewrite
.
1
|
s
.
draw
(
Point
(
1.0
,
2.0
))
:
|^
|
not
a
legal
formal
parameter
2
|
str
=>
println
(
str
)
However, there is an experimental compiler flag -Yindent-colons
that enables this capability, but it remains experimental (at the time of this writing), because it “is more contentious and less stable than the rest of the significant indentation scheme.” (Quote from this Dotty documentation.)
Back to using parentheses or braces, if we use the default value for offset
, the first set of parentheses is still required. Otherwise, the function would be parsed as the offset
, triggering an error.
s
.
draw
()
{
str
=>
println
(
str
)
}
To be clear, draw
could just have a single parameter list with two values, like Java methods. If so, the client code would look like this:
s
.
draw
(
Point
(
1.0
,
2.0
),
str
=>
println
(
str
))
That’s not nearly as clear and elegant. It would also prevent us from using the default value for the offset
.
By the way, we can can simplify our expressions even more. str => println(str)
is an anonymous function that takes a single string argument and passes it to println
. Although, println
is implemented as a method in the Scala library, it can also be used as a function that takes a single string argument! Hence, the following two lines behave the same:
s
.
draw
(
Point
(
1.0
,
2.0
))(
str
=>
println
(
str
))
s
.
draw
(
Point
(
1.0
,
2.0
))(
println
)
To be clear, the are not identical, they just do the same thing. In the first example, we pass an anonymous function that calls println
. In the second example, we use println
as a named function directly. Scala handles converting methods to functions in situations like this.
Another advantage of allowing two or more parameter lists is that we can use one or more lists for normal parameters and other lists for using clauses, formerly known as implicit parameter lists. These are parameter lists declared with the using
or implicit
keyword. When the methods are called, we can either explicitly specify arguments for these parameters, or we can let the compiler fill them in using a suitable value that’s in scope. Using clauses provides a more flexible alternative to parameters with default values. Let’s explore an example from the Scala library that uses this mechanism, Futures
.
The scala.concurrent.Future
API is another tool for concurrency. Akka uses Future
s, but you can use them separately when you don’t need the full capabilities of actors.
When you wrap some work in a Future
, the work is executed asynchronously and the Future
API provides various ways to process the results, such as providing callbacks that will be invoked when the result is ready. Let’s use callbacks here and defer discussion of the rest of the API until ToolsForConcurrency
.
The following example fires off five work items concurrently and handles the results as they finish:
// src/script/scala/progscala3/typelessdomore/Futures.scala
import
scala.concurrent.Future
import
scala.concurrent.ExecutionContext.Implicits.global
import
scala.util.
{
Failure
,
Success
}
def
sleep
(
millis
:
Long
)
=
Thread
.
sleep
(
millis
)
(
1
to
5
)
foreach
{
i
=>
val
future
=
Future
{
val
duration
=
(
math
.
random
*
1000
)
.
toLong
sleep
(
duration
)
if
i
==
3
then
throw
new
RuntimeException
(
s"
$i
->
$duration
"
)
duration
}
future
onComplete
{
case
Success
(
result
)
=>
println
(
s"
Success! #
$i
->
$result
"
)
case
Failure
(
throwable
)
=>
println
(
s"
FAILURE! #
$i
->
$throwable
"
)
}
}
sleep
(
1000
)
// Wait long enough for the "work" to finish.
println
(
"Finished!"
)
We’ll discuss this import below.
A sleep
method to simulate staying busy for a period amount of time.
Pass a block of work to the scala.concurrent.Future.apply
method. It calls sleep
with a duration
, a randomly generated number of milliseconds between 0 and 1000, which it will also return. However, if i
equals 3, we throw an exception.
Use onComplete
to assign a partial function to handle the computation result. Notice that the expected output is either scala.util.Success
wrapping a value or scala.util.Failure
wrapping an exception.
Success
and Failure
are subclasses of scala.util.Try
, which encapsulates try {…} catch {…}
clauses with less boilerplate. We can handle successful code and possible exceptions more uniformly. See TryContainer
for further discussion.
When we iterate through a Range
of integers from 1 to 5, inclusive, we construct a Future
with a block of work to do. Future.apply
returns a new Future
instance immediately. The body is executed asynchronously on another thread. The onComplete
callback we register will be invoked when the body completes.
A final sleep
call waits before exiting to allow the futures to finish.
A sample run might go like this, where the order of the results and the numbers on the right-hand side are arbitrary, as expected:
Success! #2 -> 178 Success! #1 -> 207 FAILURE! #3 -> java.lang.RuntimeException: 3 -> 617 Success! #5 -> 738 Success! #4 -> 938 Finished!
You might wonder about the “body of work” we’re passing to Future.apply
. Is it a function or something else? Here is part of the declaration of Future.apply
apply
[
T
](
body
:
=>
T
)(
/* explained below */
)
:
Future
[
T
]
Note how the type of body
is declared, => T
. This is called a by-name parameter. We are passing something that will return a T
instance, but we want to evaluate body
lazily. Go back to the example body we passed to Future.apply
above. We did not want that code evaluated before it was passed to Future.apply
. We wanted it evaluated inside the Future
after construction. This is what by-name parameters do for us. We can pass a block of code that will be evaluated only when needed. The implementation of Future.apply
evaluates this code.
Okay, let’s finally get back to implicit parameters. Note the second import statement:
import
scala.concurrent.ExecutionContext.Implicits.global
Future
methods use an ExecutionContext
to run code in separate threads, providing concurrency. These methods use a given
value of an ExecutionContext
. For example, here’s the whole Future.apply
declaration (using Scala 3 syntax):
apply
[
T
](
body
:
=>
T
)(
using
executor
:
ExecutionContext
)
:
Future
[
T
]
In the Scala 2 library, the implicit
keyword is used instead of using
. The second parameter list is called a using clause.
Because this parameter is in its own parameter list starting with using
or implicit
, users of Future.apply
don’t have to pass a value explicitly. This reduces code boilerplate. We imported the default ExecutionContext.global
value that is declared as given
(or implicit
in Scala 2). It uses a thread pool with a work-stealing algorithm to balance the load and optimize performance.
We can tailor how threads are used by passing our own ExecutionContext
explicitly:
Future
(
work
)(
using
someExecutionContext
)
Alternatively, we can declare our own given
value that will be used implicitly when Future.apply
is called:
given
myEC
as
MyCustomExecutionContext
(
arguments
)
...
val
future
=
Future
(
work
)
The global
value is declared in a similar way, but our given
value will take precedence.
The Future.onComplete
method we used also has a using
clause:
abstract
def
onComplete
[
U
](
f
:
(
Try
[
T
])
=>
U
)(
using
executor
:
ExecutionContext
)
:
Unit
So, when global
is imported into the current scope, the compiler will use it when methods are called that have a using clause with an ExecutionContext
parameter, unless we specify a value explicitly. For this to work, only given
instances that are type compatible with the parameter will be considered.
The details for the new idioms and the reasons for their existence are explained in AbstractingOverContextPart1
.
Method definitions can also be nested. This is useful when you want to refactor a lengthy method body into smaller methods, but the “helper” methods aren’t needed outside the original method. Nesting them inside the original method means they are invisible to the rest of the code base, including other methods in the type.
Here is an example for a factorial calculator:
// src/script/scala/progscala3/typelessdomore/Factorial.scala
def
factorial
(
i
:
Int
)
:
Long
=
def
fact
(
i
:
Int
,
accumulator
:
Long
)
:
Long
=
if
(
i
<=
1
)
accumulator
else
fact
(
i
-
1
,
i
*
accumulator
)
fact
(
i
,
1L
)
(
0
to
5
).
foreach
(
i
=>
println
(
s"
$i
:
${
factorial
(
i
)
}
"
))
The last line prints the following:
0: 1 1: 1 2: 2 3: 6 4: 24 5: 120
The fact
method calls itself recursively, passing an accumulator
parameter, where the result of the calculation is “accumulated.” Note that we return the accumulated value when the counter i
reaches 1. (We’re ignoring negative integer arguments, which would be invalid. The function just returns 1 for i <= 1
.) After the definition of the nested method, factorial
calls it with the passed-in value i
and the initial accumulator “seed” value of 1.
Notice that we use i
as a parameter name twice, first in the factorial
method and again in the nested fact
method. The use of i
as a parameter name for fact
“shadows” the outer use of i
as a parameter name for factorial
. This is fine, because we don’t need the outer value of i
inside fact
. We only use it the first time we call fact
, at the end of factorial
.
Like a local variable declaration in a method, a nested method is also only visible inside the enclosing method.
Look at the return types for the two functions. We used Long
because factorials grow in size quickly. So, we didn’t want Scala to infer Int
as the return type. Otherwise, we don’t need the type annotation on factorial
.
However, we must declare the return type for fact
, because it is recursive and Scala’s local-scope type inference can’t infer the return type of recursive functions.
You might be a little nervous about a recursive function. Aren’t we at risk of blowing up the stack? The JVM and many other language environments don’t do tail-call optimizations, which would convert a tail-recursive function into a loop. This prevents stack overflow and also makes execution faster by eliminating the additional function invocations.
The term tail-recursive means that the recursive call is the last thing done in an expression and only one recursive call is made. If we made the recursive call, then added something to the result, for example, that would not be a tail call. This doesn’t mean that non-tail-call recursion is disallowed, just that we can’t optimize it into a loop.
Recursion is a hallmark of functional programming and a powerful tool for writing elegant implementations of many algorithms. Hence, the Scala compiler does limited tail-call optimizations itself. It will handle functions that call themselves, but not so-called “trampoline” calls, i.e., “a calls b calls a calls b,” etc.
Still, you might want to know if you got it right and the compiler did in fact perform the optimization. No one wants a blown stack in production. Fortunately, the compiler can tell you if you got it wrong, if you add an annotation,
tailrec
, as shown in this refined version of factorial
:
// src/script/scala/progscala3/typelessdomore/FactorialTailrec.scala
import
scala.annotation.tailrec
def
factorial
(
i
:
Int
)
:
Long
=
@tailrec
def
fact
(
i
:
Int
,
accumulator
:
Long
)
:
Long
=
if
i
<=
1
then
accumulator
else
fact
(
i
-
1
,
i
*
accumulator
)
fact
(
i
,
1
)
(
0
to
5
).
foreach
(
i
=>
println
(
s"
$i
:
${
factorial
(
i
)
}
"
))
If fact
is not actually tail recursive, the compiler will throw an error. Consider this attempt to write a naïve recursive implementation of Fibonacci sequences:
// src/script/scala/progscala3/typelessdomore/FibonacciTailrec.scala
scala
>
import
scala.annotation.tailrec
scala
>
@tailrec
|
def
fibonacci
(
i
:
Int
)
:
Long
=
|
if
(
i
<=
1
)
1L
|
else
fibonacci
(
i
-
2
)
+
fibonacci
(
i
-
1
)
4
|
else
fibonacci
(
i
-
2
)
+
fibonacci
(
i
-
1
)
|
^^^^^^^^^^^^^^^^
|
Cannot
rewrite
recursive
call
:
it
is
not
in
tail
position
4
|
else
fibonacci
(
i
-
2
)
+
fibonacci
(
i
-
1
)
|
^^^^^^^^^^^^^^^^
|
Cannot
rewrite
recursive
call
:
it
is
not
in
tail
position
We are attempting to make two recursive calls, not one, and then do something with the returned values, add them. So, this function is not tail recursive. (It’s naïve because it is possible to write a tail recursive implementation.)
Finally, the nested function can see anything in scope, including arguments passed to the outer function. Note the use of n
in count
in the next example:
// src/script/scala/progscala3/typelessdomore/CountTo.scala
import
scala.annotation.tailrec
def
countTo
(
n
:
Int
)
:
Unit
=
@tailrec
def
count
(
i
:
Int
)
:
Unit
=
if
(
i
<=
n
)
then
println
(
i
)
count
(
i
+
1
)
count
(
1
)
countTo
(
5
)
Statically typed languages provide wonderful compile-time safety, but they can be very verbose if all the type information has to be explicitly provided. Scala’s type inference removes most of this explicit detail, but where it is still required, it can provide an additional benefit of documentation for the reader.
Some functional programming languages, like Haskell, can infer almost all types, because they do global type inference. Scala can’t do this, in part because Scala has to support subtype polymorphism, for object-oriented inheritance, which makes type inference harder.
We’ve already seen examples of Scala’s type inference. Here are two more examples, showing different ways to declare a Map
:
scala
>
val
map1
:
Map
[
Int
,String
]
=
Map
.
empty
val
map1
:
Map
[
Int
,String
]
=
Map
()
scala
>
val
map2
=
Map
.
empty
[
Int
,String
]
val
map1
:
Map
[
Int
,String
]
=
Map
()
The second form is more idiomatic most of the time. However, Map
is actually a trait with concrete subclasses, so you’ll sometimes make declarations like this one for a TreeMap
:
scala
>
import
scala.collection.immutable.TreeMap
scala
>
val
map3
:
Map
[
Int
,String
]
=
TreeMap
.
empty
val
map3
:
Map
[
Int
,String
]
=
Map
()
Here is a summary of the rules for when explicit type annotations are required in Scala.
The last case is somewhat rare, fortunately.
The Any
type is the root of the Scala type hierarchy. If a block of code is inferred to return a value of type Any
unexpectedly, chances are good that the code is more general than you intended so that Any
is the only common super type of all possible values.
We’ll explore Scala’s types in ScalaTypeHierarchy
.
Let’s look at a few examples of cases we haven’t seen yet where explicit types are required. First, look at overloaded methods:
// src/script/scala/progscala3/typelessdomore/MethodOverloadedReturn.scala
case
class
Money
(
value
:
Double
)
case
object
Money
{
def
apply
(
s
:
String
)
:
Money
=
apply
(
s
.
toDouble
)
def
apply
(
d
:
BigDecimal
)
:
Money
=
apply
(
d
.
toDouble
)
}
While the Money
constructor expects a Double
, we want the user to have the convenience of passing a String
or a BigDecimal
(ignoring possible errors). So, we add two more apply
methods to the companion object. Both call the apply(d: Double)
method the compiler automatically generates for the companion object, corresponding to the primary constructor Money(value: Double)
.
The two methods have explicit return types. If you try removing them, you’ll get a compiler error:
scala
>
case
class
Money
(
value
:
Double
)
|
case
object
Money
{
|
def
apply
(
s
:
String
)
=
apply
(
s
.
toDouble
)
// no return type
|
def
apply
(
d
:
BigDecimal
)
=
apply
(
d
.
toDouble
)
// no return type
|
}
4
|
def
apply
(
d
:
BigDecimal
)
=
apply
(
d
.
toDouble
)
|
^
|
Overloaded
or
recursive
method
apply
needs
return
type
3
|
def
apply
(
s:
String
)
=
apply
(
s
.
toDouble
)
|
^
|
Overloaded
or
recursive
method
apply
needs
return
type
Scala supports methods that take variable argument lists (sometimes just called variadic methods). Consider this contrived example that computes the mean of Doubles
s:
// src/script/scala/progscala3/typelessdomore/VariadicArguments.scala
scala
>
object
Mean1
{
|
def
calc1
(
ds
:
Double*
)
:
Double
=
calc2
(
ds
:_
*
)
|
def
calc2
(
ds
:
Seq
[
Double
])
:
Double
=
ds
.
sum
/
ds
.
size
|
}
scala
>
Mean1
.
calc1
(
1.0
,
2.0
)
val
res0
:
Double
=
1.5
scala
>
Mean1
.
calc2
(
Seq
(
1.0
,
2.0
))
val
res1
:
Double
=
1.5
The syntax ds: Double*
means zero or more Double
s, a variable argument list. Since calc1
calls calc2
, which expects a Seq[Double]
argument. the unusual syntax (ds :_*)
is how you take the variable argument list and convert to a sequence when needed.
Why have both functions? The examples show that both can be convenient for the user. In particular, using calc1
doesn’t require you to wrap values in a Seq
first.
There are downsides. The API “footprint” is larger with two methods instead of one and the maintenance burden is larger.
Assuming you want both methods, why not use the same name, in particular, apply
?
scala
>
object
Mean2
{
|
def
apply
(
ds
:
Double*
)
:
Double
=
apply
(
ds
:_
*
)
|
def
apply
(
ds
:
Seq
[
Double
])
:
Double
=
ds
.
sum
/
ds
.
size
|
}
3
|
def
apply
(
ds
:
Seq
[
Double
])
:
Double
=
ds
.
sum
/
ds
.
size
|
^
|
Double
definition
:
|
def
apply
(
ds:
Double*
)
:
Double
in
object
Mean2
at
line
2
and
|
def
apply
(
ds
:
Seq
[
Double
])
:
Double
in
object
Mean2
at
line
3
|
have
the
same
type
after
erasure.
In fact, ds: Double*$$
is converted to a kind of sequence internally, so effectively the methods look identical at the byte code level.
There’s a common idiom to break the ambiguity, add a first Double parameter in the first apply
, then use a variable list for the rest of the supplied arguments:
scala
>
object
Mean
{
|
def
apply
(
d
:
Double
,
ds
:
Double*
)
:
Double
=
apply
(
d
+:
ds
)
|
def
apply
(
ds
:
Seq
[
Double
])
:
Double
=
ds
.
sum
/
ds
.
size
|
}
// defined object Mean
scala
>
Mean
(
1.0
,
2.0
)
val
res10
:
Double
=
1.5
scala
>
Mean
(
Seq
(
1.0
,
2.0
))
val
res11
:
Double
=
1.5
scala
>
Mean
()
1
|
Mean
()
|^^^^
|
None
of
the
overloaded
alternatives
of
method
apply
in
object
Mean
with
types
|
(
ds
:
Seq
[
Double
])
:
Double
|
(
d
:
Double
,
ds
:
Double*
)
:
Double
|
match
arguments
()
scala
>
Mean
(
Nil
)
val
res12
:
Double
=
NaN
When calling the second apply
, the first one constructs a new sequence prepending d
to ds
using d +: ds
. We’ll explain this syntax in MatchingOnSequences
.
Finally, Nil
is an object representing an empty sequence with any type of elements.
Scala reserves some words for defining constructs like conditionals, declaring variables, etc. Some of the reserved words are marked with (soft), which means they can be used as regular identifiers for method and variable names, for example, but they are treated as keywords when used in particular contexts. All of the soft words are new reserved words in Scala 3. The reason for treating them as soft is to avoid breaking older code that happens to use them as identifiers.
reserved-words-table
lists the reserved keywords in Scala.
Many are found in Java and they usually have the same meanings in both languages.
Word | Description | See … |
---|---|---|
|
Makes a declaration abstract. |
|
|
(soft) Used with |
|
|
Start a case clause in a match expression. Define a “case class.” |
|
|
Start a clause for catching thrown exceptions. |
|
|
Start a class declaration. |
|
|
Start a method declaration. |
|
|
New syntax for |
|
|
Start an |
|
|
Indicates that the class or trait that follows is the parent type of the class or trait being declared. |
|
|
(soft) Marks one or more extension methods for a type. |
|
|
|
|
|
Applied to a class or trait to prohibit deriving child types from it. Applied to a member to prohibit overriding it in a derived class or trait. |
|
|
Start a clause that is executed after the corresponding |
|
|
Start a |
|
|
Used in Scala 2 for existential type declarations to constrain the allowed concrete types that can be used. Dropped in Scala 3. |
|
|
Marks implicit definitions. |
|
|
Start an |
|
|
Marks a method or value as eligible to be used as an implicit type converter or value. Marks a method parameter as optional, as long as a type-compatible substitute object is in the scope where the method is called. |
|
|
Import one or more types or members of types into the current scope. |
|
|
Defer evaluation of a |
|
|
Start a pattern-matching clause. |
|
|
Create a new instance of a class. |
|
|
Value of a reference variable that has not been assigned a value. |
|
|
Start a singleton declaration: a |
|
|
(soft) Declares a special type alias with zero runtime overhead. |
|
|
(soft) Declares a concrete class is open for subclassing. |
|
|
Override a concrete member of a type, as long as the original is not marked |
|
|
Start a package scope declaration. |
|
|
Restrict visibility of a declaration. |
|
|
Restrict visibility of a declaration. |
|
|
Deprecated. Was used for self-typing. |
|
|
Return from a function. |
|
|
Applied to a parent type. Requires all derived types to be declared in the same source file. |
|
|
Analogous to |
|
|
New syntax for |
|
|
How an object refers to itself. The method name for auxiliary constructors. |
|
|
Throw an exception. |
|
|
A mixin module that adds additional state and behavior to an instance of a class. Can also be used to just declare methods, but not define them, like a Java interface. |
|
|
Start a block that may throw an exception. |
|
|
|
|
|
Start a type declaration. |
|
|
(soft) Scala 3 alternative to |
|
|
Start a read-only “variable” declaration. |
|
|
Start a read-write variable declaration. |
|
|
Start a |
|
|
Include the trait that follows in the class being declared or the object being instantiated. |
|
|
Return an element in a |
|
|
A placeholder, used in imports, function literals, etc. |
|
|
Separator between identifiers and type annotations. |
|
|
Assignment. |
|
|
Used in function literals to separate the parameter list from the function body. |
|
|
Used in |
|
|
Used in parameterized and abstract type declarations to constrain the allowed types. |
|
|
Used in parameterized and abstract type “view bounds” declarations. |
|
|
Used in parameterized and abstract type declarations to constrain the allowed types. |
|
|
Used in type projections. |
|
|
Marks use of an annotation. |
Some Java methods use names that are reserved words by Scala, for example, java.util.Scanner.match
. To avoid a compilation error, surround the name with single back quotes (“back ticks”), e.g., java.util.Scanner.`match`
.
We’ve seen a few literal values already, such as val book = "Programming Scala"
, where we initialized a val book
with a String
literal, and (s: String) => s.toUpperCase
, an example of a function literal. Let’s discuss all the literals supported by Scala.
Scala 3 expands the ways that numeric literals can be written and used as initializers. Consider these examples:
val
i
:
Int
=
123
// decimal
val
x
:
Long
=
0x123
L
// hexadecimal (291 decimal)
val
f
:
Float
=
123
_456
.
789
F
// 123456.789
val
d
:
Double
=
123
_456_789
.
0123
// 123456789.0123
val
y
:
BigInt
=
0x123
_a4b_c5d_e6f_789
// 82090347159025545
val
z
:
BigDecimal
=
123
_456_789
.
0123
// 123456789.0123
Scala 3 allows underscores to make long numbers easier to read. They can appear anywhere in the literal (except between 0x
), not just between every third character.
Hexadecimal numbers start with 0x
followed by one or more digits and the letters a
through f
and A
through F
.
Indicate a negative number by prefixing the literal with a –
sign.
The ability to use numeric literals for library and user-defined types like BigInt
and BigDecimal
is implemented with a new trait called
FromDigits
.
For Long
literals, it is necessary to append the L
or l
character at the end of the literal, unless you are assigning the value to a variable declared to be Long
. Otherwise, Int
is inferred. The valid values for an integer literal are bounded by the type of the variable to which the value will be assigned. integer-boundaries-table
defines the limits, which are inclusive.
Target type | Minimum (inclusive) | Maximum (inclusive) |
---|---|---|
|
−263 |
263 − 1 |
|
−231 |
231 − 1 |
|
−215 |
215 − 1 |
|
0 |
216 − 1 |
|
−27 |
27 − 1 |
A compile-time error occurs if an integer literal number is specified that is outside these ranges.
Floating-point literals are expressions with an optional minus sign, zero or more digits and underscores, followed by a period (.
), followed by one or more digits. For Float
literals, append the F
or f
character at the end of the literal. Otherwise, a Double
is assumed. You can optionally append a D
or d
for a Double
.
Floating-point literals can be expressed with or without exponentials. The format of the exponential part is e
or E
, followed by an optional +
or –
, followed by one or more digits.
Here are some example floating-point literals, where Double
is inferred unless the declared variable is Float
or an f
or F
suffix is used:
0.14
3.14
3.14f
3.14F
3.14d
3.14D
3
e5
3
E5
3.14e+5
3.14e-5
3.14e-5f
3.14e-5F
3.14e-5d
3.14e-5D
At least one digit must appear after the period, 3.
and 3.e5
are disallowed. Use 3.0
and 3.0e5
instead. Otherwise it would be ambiguous; do you mean method e5
on the Int
value of 3
or do you mean floating point literal 3.0e5
?
Float
consists of all IEEE 754 32-bit, single-precision binary floating-point values. Double
consists of all IEEE 754 64-bit, double-precision binary floating-point values.
A character literal is either a printable Unicode character or an escape sequence, written between single quotes. A character with a Unicode value between 0 and 255 may also be represented by an octal escape, i.e., a backslash () followed by a sequence of up to three octal characters. It is a compile-time error if a backslash character in a character or string literal does not start a valid escape sequence.
Here are some examples:
'A
'
'
u0041
'
// 'A' in Unicode
' '
'
012
'
// ' ' in octal
' '
The valid escape sequences are shown in char-escape-sequences-table
.
Sequence | Meaning |
---|---|
Backspace (BS) |
|
|
Horizontal tab (HT) |
|
Line feed (LF) |
|
Form feed (FF) |
|
Carriage return (CR) |
|
Double quote ( |
|
Single quote ( |
|
Backslash ( |
Note that nonprintable Unicode characters like u0009
(tab) are not allowed. Use the equivalents like
. Recall that three Unicode characters were mentioned in reserved-words-table
as valid replacements for corresponding ASCII sequences, ⇒ for =>
, → for ->
, and ← for <-
.
A string literal is a sequence of characters enclosed in double quotes or triples of double quotes, i.e., """…"""
.
For string literals in double quotes, the allowed characters are the same as the character literals. However, if a double quote ("
) character appears in the string, it must be “escaped” with a character. Here are some examples:
"Programming Scala"
"He exclaimed, "Scala is great!""
"First Second"
The string literals bounded by triples of double quotes are also called multiline string literals. These strings can cover several lines; the line feeds will be part of the string. They can include any characters, including one or two double quotes together, but not three together. They are useful for strings with characters that don’t form valid Unicode or escape sequences, like the valid sequences listed in
char-escape-sequences-table
. Regular expressions are a good example, which use lots of escaped characters with special meanings. Conversely, if escape sequences appear, they aren’t interpreted.
Here are three example strings:
"""Programming Scala"""
"""He exclaimed, "Scala is great!" """
"""First line
Second line
Fourth line"""
Note that we had to add a space before the trailing """
in the second example to prevent a parse error. Trying to escape the second "
that ends the "Scala is great!"
quote, i.e., "Scala is great!"
, doesn’t work.
When using multiline strings in code, you’ll want to indent the substrings for proper code formatting, yet you probably don’t want that extra whitespace in the actual string output. String.stripMargin
solves this problem. It removes all whitespace in the substrings up to and including the first occurrence of a vertical bar, |
. If you want some whitespace indentation, put the whitespace you want after the |
. Consider this example:
// src/script/scala/progscala3/typelessdomore/MultilineStrings.scala
def
hello
(
name
:
String
)
=
s"""Welcome!
Hello,
$name
!
* (Gratuitous Star!!)
|We're glad you're here.
| Have some extra whitespace."""
.
stripMargin
val
hi
=
hello
(
"Programming Scala"
)
The last line prints the following:
val hi: String = Welcome! Hello, Programming Scala! * (Gratuitous Star!!) We're glad you're here. Have some extra whitespace.
Note where leading whitespace is removed and where it isn’t.
If you want to use a different leading character than |
, use the overloaded version of stripMargin
that takes a Char
(character) parameter. If the whole string has a prefix or suffix you want to remove (but not on individual lines), there are corresponding stripPrefix
and stripSuffix
methods, too:
scala
>
"<hello> <world>"
.
stripPrefix
(
"<"
).
stripSuffix
(
">"
)
val
res0
:
String
=
hello
>
<
world
Scala 2 supported symbols, which are interned strings, meaning that two symbols with the same “name” (i.e., the same character sequence) will actually refer to the same object in memory. They are deprecated, but still exist in Scala 3. However, the literal syntax has been removed:
scala
>
val
sym1
=
'name
// Scala 2 only; single "tick"
scala
>
val
sym2
=
Symbol
(
"name"
)
// Scala 2 and 3
You might see symbols in older code, but don’t use them yourself.
As we’ve seen already, (i: Int, d: Double) => (i+d).toString
is a function literal of type Function2[Int,Double,String]
, where the last type is the return type.
You can even use the literal syntax for a type declaration. The following declarations are equivalent:
val
f1
:
(
Int
,
String
)
=>
String
=
(
i
,
s
)
=>
s
+
i
val
f2
:
Function2
[
Int
,String
,String
]
=
(
i
,
s
)
=>
s
+
i
Often, declaring a class to hold instances with two or more values is more than you need. You could put those values in a collection, but then you lose their specific type information. Scala implements tuples of values, where the individual types are retained. A literal syntax for tuples uses a comma-separated list of values surrounded by parentheses.
Here is an example of a tuple declaration and how we can access elements and use pattern matching to extract them:
// src/script/scala/progscala3/typelessdomore/TupleExample.scala
scala
>
val
tup
=
(
"Hello"
,
1
,
2.3
)
val
tup
:
(
String
,
Int
,
Double
)
=
(
Hello
,
1
,
2.3
)
scala
>
val
tup2
:
(
String
,
Int
,
Double
)
=
(
"World"
,
4
,
5.6
)
val
tup2
:
(
String
,
Int
,
Double
)
=
(
World
,
4
,
5.6
)
scala
>
tup
.
_1
val
res0
:
String
=
Hello
scala
>
tup
.
_2
val
res1
:
Int
=
1
scala
>
tup
.
_3
val
res2
:
Double
=
2.3
scala
>
val
(
s
,
i
,
d
)
=
tup
val
s
:
String
=
Hello
val
i
:
Int
=
1
val
d
:
Double
=
2.3
scala
>
println
(
s"
s =
$s
, i =
$i
, d =
$d
"
)
s
=
Hello
,
i
=
1
,
d
=
2.3
Use the literal syntax to construct a three-element tuple. Note the literal syntax is used for the type, too.
Extract the first element of the tuple. Tuple indexing is one-based, by historical convention, not zero-based. The next two lines extract the second and third elements.
Declare three values, s
, i
, and d
, that are assigned the three corresponding fields from the tuple using pattern matching.
Two-element tuples, sometimes called pairs for short, are so commonly used there is a special syntax for constructing them:
scala
>
(
1
,
"one"
)
val
res3
:
(
Int
,
String
)
=
(
1
,
one
)
scala
>
1
->
"one"
val
res4
:
(
Int
,
String
)
=
(
1
,
one
)
scala
>
Tuple2
(
1
,
"one"
)
// Rarely used.
val
res5
:
(
Int
,
String
)
=
(
1
,
one
)
For example, maps are often constructed with key-value pairs as follows:
// src/script/scala/progscala3/typelessdomore/StateCapitalsSubset.scala
scala
>
val
stateCapitals
=
Map
(
|
"Alabama"
->
"Montgomery"
,
|
"Alaska"
->
"Juneau"
,
|
// ...
|
"Wyoming"
->
"Cheyenne"
)
val
stateCapitals
:
Map
[
String
,String
]
=
Map
(
Alabama
->
Montgomery
,
Alaska
->
Juneau
,
Wyoming
->
Cheyenne
)
Let’s discuss three useful types that express a very useful concept, when we may or may not have a value.
Most languages have a special keyword or type instance that’s assigned to reference variables when there’s nothing else for them to refer to. In Scala and Java, it’s called null
.
Using null
is a giant source of nasty bugs. What null
really signals is that we don’t have a value in a given situation. If the value is not null
, we do have a value. Why not express this situation explicitly with the type system and exploit type checking to avoid NullPointerException
s?
Option
lets us express this situation explicitly without using null
. Option
is an abstract class and its two concrete subclasses are Some
, for when we have a value, and None
, when we don’t.
You can see Option
, Some
, and None
in action using the map of state capitals in the United States that we declared in the previous section:
scala
>
stateCapitals
.
get
(
"Alabama"
)
|
stateCapitals
.
get
(
"Wyoming"
)
|
stateCapitals
.
get
(
"Unknown"
)
val
res6
:
Option
[
String
]
=
Some
(
Montgomery
)
val
res7
:
Option
[
String
]
=
Some
(
Cheyenne
)
val
res8
:
Option
[
String
]
=
None
scala
>
stateCapitals
.
getOrElse
(
"Alabama"
,
"Oops1"
)
|
stateCapitals
.
getOrElse
(
"Wyoming"
,
"Oops2"
)
|
stateCapitals
.
getOrElse
(
"Unknown"
,
"Oops3"
)
val
res9
:
String
=
Montgomery
val
res10
:
String
=
Cheyenne
val
res11
:
String
=
Oops3
Map.get
returns an Option[T]
, where T
is String
in this case. Either a Some
wrapping the value is returned or a None
when no value for the specified key was found.
In contrast, similar methods in other languages just return a T
value, when found, or null
.
By returning an Option
, we can’t “forget” that we have to verify that something was returned. In other words, the fact that a value may not exist for a given key is enshrined in the return type for the method declaration. This also provides clear documentation for the user of Map.get
about what can be returned.
The second group uses Map.getOrElse
. This method returns either the value found for the key or it returns the second argument passed in, which functions as the default value to return.
So, getOrElse
is more convenient, as you don’t need to process the Option
, if a suitable default value exists.
To reiterate, because the Map.get
method returns an Option
, it automatically documents for the reader that there may not be an item matching the specified key. The map handles this situation by returning a None
.
Also, thanks to Scala’s static typing, you can’t make the mistake of “forgetting” that an Option
is returned and attempting to call a method supported by the type of the value inside the Option
. You must extract the value first or handle the None
case. Without an option return type, when a method just returns a value, it’s easy to forget to check for null
before calling methods on the returned object.
Because Scala runs on the JVM, JavaScript, and native environments, it must interoperate with other libraries, which means Scala has to support null
.
Scala 3 introduces a new way to indicate a possible null
through the type Scala.Null
, which is a subtype of all other types. Suppose you have a Java HashMap
to access:
// src/script/scala/progscala3/typelessdomore/Null.scala
import
java.util.
{
HashMap
=>
JHashMap
}
val
jhm
=
new
JHashMap
[
String
,
String
]
(
)
jhm
.
put
(
"one"
,
"1"
)
val
one1
:
String
=
jhm
.
get
(
"one"
)
val
one2
:
String
|
Null
=
jhm
.
get
(
"one"
)
val
two1
:
String
=
jhm
.
get
(
"two"
)
val
two2
:
String
|
Null
=
jhm
.
get
(
"two"
)
Import the Java HashMap
, but give it an alias so it doesn’t shadow Scala’s HashMap
.
Return the string “1”.
Declare explicitly that one2
is of type String
or Null
. The value will still be “1” in this case.
These two values will equal null
.
Scala 3 introduces union types, which we use for one2
and two2
. They tell the reader that the value could be either a String
or a null
.
There is an optional feature to enable aggressive null checking. If you add the flag -Yexplicit-nulls
, then the declarations of one1
and two1
will be disallowed, because the compiler knows you are referring to a Java library where null
could be returned. I have not enabled this option in the code examples build, because it forces lots of changes to otherwise safe code and it has other implications that are described in more detail in the documentation. However, if you use this same code in a REPL with this flag, you’ll see the following:
$
scala
-
Yexplicit
-
nulls
...
scala
>
val
one1
:
String
=
jhm
.
get
(
"one"
)
1
|
val
one1
:
String
=
jhm
.
get
(
"one"
)
|
^^^^^^^^^^^^^^
|
Found
:
String
|
UncheckedNull
|
Required
:
String
...
Tony Hoare, who invented the null reference in 1965 while working on a language called ALGOL W, called its invention his “billion dollar” mistake. Use Option
instead. Consider enabling explicit nulls.
While we’re discussing Option
, let’s discuss a useful design feature it uses. A key point about Option
is that there are really only two valid subtypes. Either we have a value, the Some
case, or we don’t, the None
case. There are no other subtypes of Option
that would be valid. So, we would really like to prevent users from creating their own.
Scala 2 and 3 have a keyword sealed
for this purpose. Option
could be declared as follows:
sealed
abstract
class
Option
[
+A
]
{...}
final
case
class
Some
[
+A
](
a
:
A
)
extends
Option
[
A
]
{...}
final
case
object
None
extends
Option
[
Nothing
]
{...}
The sealed
keyword tells the compiler that all subclasses must be declared in the same source file. Some
and None
are declared in the same file with Option
in the Scala library. This technique effectively prevents additional subtypes of Option
.
None
has an interesting declaration. It is a case class with only one instance, so it is declared case object
. The Nothing
type along with the Null
type are subtypes of all other types in Scala. We’ll explain Nothing
in more detail in SeqsInFunctionalProgramming
, if you get what I mean…
You can also declare a type final
if you want to prevent users from subtyping it. If you want to encourage subtyping of a concrete class, you can declare it open
. (This is redundant for abstract types.)
This same constraint on subclassing can now be achieved more concise in Scala 3 with the new enum
syntax:
enum
Option
[
+A
]
{
case
Some
(
a
:
A
)
{...}
case
None
{...}
...
}
Scala adopts the package
concept that Java uses for namespaces, but Scala offers more flexibility. Filenames don’t have to match the type names and the package structure does not have to match the directory structure. So, you can define packages in files independent of their “physical” location.
The following example defines a class MyClass
in a package com.example.mypkg
using the conventional Java syntax:
// src/main/scala/progscala3/typelessdomore/PackageExample1.scala
package
com.example.mypkg
class
MyClass
:
def
mymethod
(
s:
String
)
:
String
=
s
Scala also supports a block-structured syntax for declaring package scope:
// src/main/scala/progscala3/typelessdomore/PackageExample2.scala
package
com
:
package
example:
package
pkg1
:
class
Class11:
def
m
=
"m11"
class
Class12
:
def
m
=
"m12"
package
pkg2
:
class
Class21:
def
m
=
"m21"
def
makeClass11
=
new
pkg1
.
Class11
def
makeClass12
=
new
pkg1
.
Class12
package
pkg3.pkg31.pkg311
:
class
Class311:
def
m
=
"m21"
Two packages, pkg1
and pkg2
, are defined under the com.example
package. A total of three classes are defined between the two packages. The makeClass11
and makeClass12
methods in Class21
illustrate how to reference a type in the “sibling” package, pkg1
. You can also reference these classes by their full paths, com.example.pkg1.Class11
and com.example.pkg1.Class12
, respectively.
The package pkg3.pkg31.pkg311
shows that you can “chain” several packages together in one statement. It is not necessary to use a separate package
statement for each package.
If you have package-level declarations, like types, in each of several parent packages that you want to bring into scope, use separate package statements as shown for each level of the package hierarchy with these declarations. Each subsequent package statement is interpreted as a subpackage of the previously specified package, as if we used the block-structure syntax shown previously. The first statement is interpreted as an absolete path.
Following the convention used by Java, the root package for Scala’s library classes is named scala
.
Although the package declaration syntax is flexible, one limitation is that packages cannot be defined within classes and objects, which wouldn’t make much sense anyway.
Scala does not allow package declarations in scripts, which are implicitly wrapped in an object
, where package declarations are not permitted.
To use declarations in packages, you have to import them. However, Scala offers flexible options how items are imported:
import
java.awt._
import
java.io.File
import
java.io.File._
import
java.util.
{
Map
,
HashMap
}
Import everything in a package, using underscore ( _
) as a wildcard.
Import an individual type.
Import all static member fields and methods in File
.
Selectively import two types from java.util
.
Java uses the asterisk character (*
) as the wildcard for imports. In Scala, this character is allowed as a method name (e.g., for multiplication), so _
is used instead to avoid ambiguity.
The third line imports all the static methods and fields in java.io.File
. The equivalent Java import statement would be import static java.io.File.*;
. Scala doesn’t have an import static
construct because it treats object
types uniformly like other types.
You can put import statements almost anywhere, so you can scope their visibility to just where they are needed, you can rename types as you import them, and you can suppress the visibility of unwanted types:
def
stuffWithBigInteger
(
)
=
{
import
java.math.BigInteger.
{
ONE
=>
_
,
TEN
,
ZERO
=>
JAVAZERO
}
// println( "ONE: "+ONE ) // ONE is effectively undefined
println
(
"TEN: "
+
TEN
)
println
(
"ZERO: "
+
JAVAZERO
)
}
Alias to _
to make it invisible. Use this technique when you want to import everything except a few items.
Import TEN
from BigDecimal
. It can be referenced simply as TEN
.
Import ZERO
but give it an alias. Use this technique to avoid shadowing other items with the same name. This is used a lot when mixing Java and Scala types, such as Java’s List
and Scala’s List
.
Because this import statement is inside stuffWithBigInteger
, the imported items are not visible outside the function.
Finally, Scala 3 adds new ways to control how implicit definitions are imported. We’ll discuss these details in GivensAndImports
, once we understand the new syntax and behaviors for given instances.
Sometimes it’s nice to give the user one import statement for a public API that brings in all types, as well as constants and methods not attached to a type. For example:
import
progscala3.typelessdomore.api._
This is simple to do; just define anything you need under the package:
// src/main/scala/progscala3/typelessdomore/PackageObjects.scala
package
progscala3.typelessdomore.api
val
DEFAULT_COUNT
=
5
def
countTo
(
limit
:
Int
=
DEFAULT_COUNT
)
=
(
0
to
limit
).
foreach
(
println
)
class
Class1
:
def
m
=
"cm1"
object
Object1
:
def
m
=
"om1"
In Scala 2, non-type definitions had to be declared inside a package object, like this:
// src/main/scala-2/progscala3/typelessdomore/PackageObjects.scala
package
progscala3.typelessdomore
// Notice, no ".api"
package
object
api
{
val
DEFAULT_COUNT
=
5
def
countTo
(
limit
:
Int
=
DEFAULT_COUNT
)
=
(
0
to
limit
).
foreach
(
println
)
class
Class1
{
def
m
=
"cm1"
}
object
Object1
{
def
m
=
"om1"
}
}
Package objects are still supported in Scala 3, but they are deprecated.
We mentioned in ATasteOfScala
that Scala supports parameterized types, which are very similar to generics in Java. (The terms are somewhat interchangeable, but the Scala community uses parameterized types.) Java uses angle brackets (<…>
), while Scala uses square brackets ([…]
), because <
and >
are often used for method names.
Because we can plug in almost any type for a type parameter A
in a collection like List[A]
, this feature is called parametric polymorphism, because generic implementations of the List
methods can be used with instances of any type A
.
Consider the declaration of Map
, which is written as follows, where K
is the keys type and V
is the values type.
trait
Map
[
K
,+V
]
extends
Iterable
[(
K
,V
)]
with
...
The +
in front of the V
means that Map[K, V2]
is a subtype of Map[K, V1]
for any V2
that is a _subtype of V1
. This is called covariant typing. It is a reasonably intuitive idea. If we have a function f(map: Map[String, Any])
, it makes sense that passing a Map[String, Double]
to it should work fine, because the function has to assume values of Any
, a parent type of Double
.
In contrast, the key K
is invariant. We can’t pass Map[Any, Any]
to f
nor any Map[S, Any]
for some subtype or supertype S
of String
.
If there is a –
in front of a type parameter, the relationship goes the other way; Foo[B]
would be a supertype of Foo[A]
, if B
is a subtype of A
and the declaration is Foo[-A]
(called contravariant typing). This is less intuitive, but also not as important to understand now. We’ll see how it is important for function types in ParameterizedTypes
.
Scala supports another type abstraction mechanism called abstract types, which can be applied to many of the same design problems for which parameterized types are used. However, while the two mechanisms overlap, they are not redundant. Each has strengths and weaknesses for certain design problems.
These types are declared as members of other types, just like methods and fields. Here is an example that uses an abstract type in a parent class, then makes the type member concrete in child classes:
// src/main/scala/progscala3/typelessdomore/AbstractTypes.scala
package
progscala3.typelessdomore
import
scala.io.Source
abstract
class
BulkReader
:
type
In
val
source
:
In
/*
*
Read source and return a sequence of Strings
*/
def
read
:
Seq
[
String
]
case
class
StringBulkReader
(
source
:
String
)
extends
BulkReader
:
type
In
=
String
def
read
:
Seq
[
String
]
=
Seq
(
source
)
case
class
FileBulkReader
(
source
:
Source
)
extends
BulkReader
:
type
In
=
Source
def
read
:
Seq
[
String
]
=
source
.
getLines
.
toVector
Abstract type, really just like any abstract field or method.
Concrete subtype of BulkReader
where In
is defined to be String
. Note that the type of the source
parameter must match.
Concrete subtype of BulkReader
where In
is defined to be Source
, the Scala library class for reading sources, like files. Source.getLines
returns an iterator, which we can easy read into a Vector
with toVector
.
Strictly speaking, we don’t need to declare the source
field in the parent class, but I put it there to show you that the concrete case classes can make it a constructor parameter, where the specific type is specified.
Using these readers:
scala
>
import
progscala3.typelessdomore.
{
StringBulkReader
,
FileBulkReader
}
scala
>
new
StringBulkReader
(
"Hello Scala!"
).
read
val
res1
:
Seq
[
String
]
=
List
(
Hello
Scala
!)
scala
>
val
lines
=
FileBulkReader
(
Source
.
fromFile
(
"README.md"
)).
read
val
lines
:
Seq
[
String
]
=
Vector
(
#
Programming
Scala
,
3
rd
Edition
,
...)
scala
>
lines
(
0
)
// look at two lines...
|
lines
(
2
)
val
res2
:
String
=
#
Programming
Scala
,
3
rd
Edition
val
res3
:
String
=
#
#
README
for
the
Code
Examples
The type
field is used like a type parameter in a parameterized type. In fact, as an exercise, try rewriting the example to use type parameters, e.g., BulkReader[In]
.
So what’s the advantage here of using type members instead of parameterized types? The latter are best for the case where the type parameter has no relationship with the parameterized type, like List[A]
when A
is Int
, String
, Person
, etc. A type member works best when it “evolves” in parallel with the enclosing type, as in our BulkReader
example, where the type member needed to match the “behaviors” expressed by the enclosing type, specifically the read
method. Sometimes this characteristic is called family polymorphism or covariant specialization.
For completeness, another use for type members is to provide a convenient alias for a more complicated type. For example, suppose you use (String, Double)
tuples a lot in some code. You could either declare a class for it or use a type alias for a simple alternative:
scala
>
object
Foo
:
|
type
Record
=
(
String
,
Double
)
|
def
transform
(
record
:
Record
)
:
Record
=
|
(
record
.
_1
.
toUpperCase
,
2
*
record
.
_2
)
// defined object Foo
scala
>
Foo
.
transform
(
"hello"
,
10
)
val
res0
:
Foo.Record
=
(
HELLO
,
20.0
)
Notice the type shown for res0
. In fact, concrete type members are always type aliases.
We covered a lot of practical ground, such as literals, keywords, file organization, and imports. We learned how to declare variables, methods, and classes. We learned about Option
as a better tool than null
, plus other useful techniques. In the next chapter, we will finish our fast tour of the Scala “basics” before we dive into more detailed explanations of Scala’s features.
18.223.152.215