Let’s finish our survey of essential “basics” in Scala.
Almost all “operators” are actually methods. Consider this most basic of examples:
1
+
2
The plus sign between the numbers is a method on the Int
type.
Scala doesn’t have special “primitives” for numbers and booleans that are distinct from types you define. They are regular types: Float
, Double
, Int
, Long
, Short
, Byte
, Char
, and Boolean
. Hence, they can have methods. However, Scala will compile to native platform primitives when possible for greater efficiency.
Also we’ve already seen that Scala identifiers can have most nonalphanumeric characters and single-parameter methods can be invoked with infix operator notation. So, 1 + 2
is the same as 1.+(2)
.
Actually, they don’t always behave identically, due to operator precedence rules. While 1 + 2 * 3 = 7
, 1.+(2)*3 = 9
. When present, the period binds before the star.
In fact, you aren’t limited to “operator-like” method names when using infix notation. It is common to write code like the following, where the dot and parentheses around println
are omitted:
scala
>
Seq
(
"one"
,
"two"
)
foreach
println
one
two
Use this convention cautiously, as these expressions can sometimes be confusing to read or parse.
To bring a little more discipline to this practice, Scala 3 now issue a deprecation warning if a one-parameter method is declared without the annotation @infix
, but is used with infix notation. However, to support traditional practice, most of the familiar collection “operators”, like map
, foreach
, etc. are declared with @infix
, so our foreach
example still works in Scala 3 without a warning.
If you still want to use infix notation and you want to avoid the deprecation warning for a method that isn’t annotated with @infix
, put the name in “back ticks”:
// src/script/scala/progscala3/rounding/InfixMethod.scala
case
class
Foo
(
str
:
String
)
:
def
append
(
s
:
String
)
:
Foo
=
copy
(
str
+
s
)
Foo
(
"one"
)
.
append
(
"two"
)
Foo
(
"one"
)
append
"two"
Foo
(
"one"
)
`append`
"two"
You can also define your own operator methods with symbolic names. Suppose you want to allow users to create java.io.File
objects by appending strings using /
, the file separator for UNIX-derived systems. Consider the following implementation:
// src/main/scala/progscala3/rounding/Path.scala
package
progscala3.rounding
import
scala.annotation.alpha
import
java.io.File
case
class
Path
(
value
:
String
,
separator
:
String
=
Path
.
defaultSeparator
)
:
val
file
=
new
File
(
value
)
override
def
toString
:
String
=
file
.
getPath
@alpha
(
"concat"
)
def
/
(
node
:
String
)
:
Path
=
copy
(
value
+
separator
+
node
)
object
Path
:
val
defaultSeparator
=
sys
.
props
(
"file.separator"
)
Use the operating systems default path separator string as the default separator when constructing a File
object.
How to override the default toString
method. Here, I use the path string from File
.
I’ll explain the @alpha
in a moment.
Use the case class copy method to create a new instance, changing only the value
.
Now users can create new File
objects as follows:
scala
>
import
progscala3.rounding.Path
scala
>
val
one
=
Path
(
"one"
)
val
one
:
progscala3.rounding.Path
=
one
scala
>
val
three
=
one
/
"two"
/
"three"
val
three
:
progscala3.rounding.Path
=
one
/
two
/
three
scala
>
three
.
file
val
res0
:
java.io.File
=
one
/
two
/
three
scala
>
val
threeb
=
one
./(
"two"
)./(
"three"
)
val
threeb
:
progscala3.rounding.Path
=
one
/
two
/
three
scala
>
three
==
threeb
val
res1
:
Boolean
=
true
scala
>
one
`concat`
"two"
1
|
one
`concat`
"two"
|^^^^^^^^^^^^
|
value
concat
is
not
a
member
of
progscala3
.
rounding
.
Path
On Windows, the character would be used as the default separator. This method is designed to be used with infix notation. It looks odd to use normal invocation syntax.
In Scala 3, the @alpha
annotation is recommended for methods with symbolic names. It will be required in a future release of Scala 3. In this example, concat
is the name the compiler will use internally when it generates byte code.
This is the name you would use if you wanted to call the method from Java code, which doesn’t support invoking methods with symbolic names. However, the name concat
can’t be used in Scala code, as shown at the end of the example. It only affects the byte code produced by the compiler.
The @infix
annotation is not required for methods that use “operator” characters, like *
and /
, because support for symbolic operators has always existed for the particular purpose of allowing intuitive, infix expressions, like a * b
and path1 / path2
.
Types can also be written with infix notation, in many contexts. The same rules using the @infix
annotation apply:
// src/script/scala/progscala3/rounding/InfixType.scala
import
scala.annotation.
{
alpha
,
infix
}
@alpha
(
"BangBang"
)
case
class
!!
[
A
,
B
]
(
a
:
A
,
b
:
B
)
val
ab1
:
Int
!!
String
=
1
!!
"one"
val
ab2
:
Int
!!
String
=
!!
(
1
,
"one"
)
@infix
case
class
bangbang
[
A
,
B
]
(
a
:
A
,
b
:
B
)
val
ab1
:
Int
bangbang
String
=
1
bangbang
"one"
val
ab2
:
Int
bangbang
String
=
bangbang
(
1
,
"one"
)
Some type declaration with two type parameters.
An attempt to use infix notation on both sides, but we get an error that !!
is not a method on Int
. We’ll solve this problem in AbstractingOverContextPart1
.
This declaration works, with the non-infix notation on the right-hand side.
These three lines behave the same, but note the use of @infix
now.
To recap:
Annotate symbolic operator definitions with @alpha("some_name")
.
Annotate alphanumeric types and methods with @infix
if you want to allow their use with infix notation.
Related to infix notation is postfix notation, where a method with no parameters can be invoked without the period. However, postfix invocations can sometimes be even more ambiguous and confusing, so Scala requires that you explicitly enable this feature first, in one of several ways:
Add the import statement import scala.language.postfixOps
to the source code.
Invoke the compiler with the option -language:postfixOps
“feature flag”.
There is no @postfix
annotation.
The SBT build for the examples is configured to use the -feature
flag that enables warnings when features are used, like postfix expressions, that should be enabled explicitly.
Dropping the punctuation by using infix or even postfix expresses can make your code cleaner and help create elegant programs that read more naturally, but avoid cases that are actually harder to understand.
Here is a summary of the rules for characters in identifiers for method and type names, variables, etc:
Scala allows all the printable ASCII characters, such as letters, digits, the underscore ( _
), and the dollar sign ($
), with the exceptions of the “parenthetical” characters, (
, )
, [
, ]
, {
, and }
, and the “delimiter” characters, `
, ’
, '
, "
, .
, ;
, and ,
. Scala allows the characters between u0020 and u007F that are not in the sets just shown, such as mathematical symbols, the so-called operator characters like /
and <
, and some other symbols. This includes white space characters, discussed below.
We listed the reserved words in ReservedWords
. Recall that some of them are combinations of operator and punctuation characters. For example, a single underscore ( _
) is a reserved word!
$
, _
, and operatorsA plain identifier can begin with a letter or underscore, followed by more letters, digits, underscores, and dollar signs. Unicode-equivalent characters are also allowed. Scala reserves the dollar sign for internal use, so you shouldn’t use it in your own identifiers, although this isn’t prevented by the compiler. After an underscore, you can have either letters and digits, or a sequence of operator characters. The underscore is important. It tells the compiler to treat all the characters up to the next whitespace as part of the identifier. For example, val xyz_++= = 1
assigns the variable xyz_++=
the value 1
, while the expression val xyz++= = 1
won’t compile because the “identifier” could also be interpreted as xyz ++=
, which looks like an attempt to append something to xyz
. Similarly, if you have operator characters after the underscore, you can’t mix them with letters and digits. This restriction prevents ambiguous expressions like this: abc_-123
. Is that an identifier abc_-123
or an attempt to subtract 123
from abc_
?
If an identifier begins with an operator character, the rest of the characters must be operator characters.
An identifier can also be an arbitrary string between two back quote characters, e.g., def `test that addition works` = assert(1 + 1 == 2)
. (Using this trick for literate test names is the one use I can think of for this otherwise-questionable technique for using white space in identifiers.) Also use back quotes to invoke a method or variable in a Java class when the name is identical to a Scala reserved word, e.g., java.net.Proxy.‵type‵()
.
In pattern-matching expressions (for example, ASampleApplication
), tokens that begin with a lowercase letter are parsed as variable identifiers, while tokens that begin with an uppercase letter are parsed as constant identifiers (such as class names). This restriction prevents some ambiguities because of the very succinct variable syntax that is used, e.g., no val
keyword is present.
Once you know that all operators are methods, it’s easier to reason about unfamiliar Scala code. You don’t have to worry about special cases when you see new operators. We’ve seen several examples where infix expressions like matrix1 * matrix2
where used, which are actually just ordinary method invocations.
This flexible method naming gives you the power to write libraries that feel like a natural extension of Scala itself. You can write a new math library with numeric types that accept all the usual mathematical operators. The possibilities are constrained by just a few limitations for method names.
Scala is flexible about the use of parentheses in methods with no parameters.
If a method takes no parameters, you can define it without parentheses. Callers must invoke the method without parentheses. Conversely, if you add empty parentheses to your definition, callers must add the parentheses.
For example, List.size
has no parentheses, so you write List(1, 2, 3).size
. If you try List(1, 2, 3).size()
, you’ll get an error.
However, exceptions are made for no-parameter methods in Java libraries. For example, the length
method for java.lang.String
does have parentheses in its definition, because Java requires them, but Scala lets you write both "hello".length()
and "hello".length
. This flexibility with Scala-defined methods was also allowed in earlier releases of Scala, but Scala 3 now requires that usage match the definition.
A convention in the Scala community is to omit parentheses for no-parameter methods that have no side effects, like returning the size of a collection, which could actually be a precomputed, immutable field in the object. So, many no-parameter methods could be interpreted as simply reading a field. However, when the method performs side effects or does extensive work, parentheses are added by convention, providing a hint to the reader of nontrivial activity, for example myFileReader.readLines()
.
Why bother with optional parentheses in the first place? They make some method call chains read better as expressive, self-explanatory “sentences” of code:
// src/script/scala/progscala3/rounding/NoDotBetter.scala
def
isEven
(
n
:
Int
)
=
(
n
%
2
)
==
0
Seq
(
1
,
2
,
3
,
4
)
filter
isEven
foreach
println
The second line is “clean”, but on the edge of non-obvious to read. Here is the same code repeated four times with progressively less explicit detail filled in. The last line is the original:
Seq
(
1
,
2
,
3
,
4
).
filter
((
i
:
Int
)
=>
isEven
(
i
)).
foreach
((
i
:
Int
)
=>
println
(
i
))
Seq
(
1
,
2
,
3
,
4
).
filter
(
i
=>
isEven
(
i
)).
foreach
(
i
=>
println
(
i
))
Seq
(
1
,
2
,
3
,
4
).
filter
(
isEven
).
foreach
(
println
)
Seq
(
1
,
2
,
3
,
4
)
filter
isEven
foreach
println
The first three versions are more explicit and hence better for the beginning reader to understand. However, once you’re familiar with the fact that filter
is a method on collections that takes a single argument, foreach
loops over a collection, and so forth, the last, “Spartan” implementation is much faster to read and understand. The other two versions have more visual noise that just get in the way, once you’re more experienced. Keep that in mind as you learn to read Scala code.
To be clear, this expression works because each method we used took a single parameter. If you tried to use a method in the chain that takes zero or more than one parameter, it would confuse the compiler. In those cases, put some or all of the punctuation back in.
The previous explanation glossed over an important detail. The four versions are not exactly the same code with different details inferred by the compiler, although all four behave the same way. Consider the function arguments passed to filter
in the second and third examples. They are different as follows:
filter(i => isEven(i))
passes an anonymous function that calls isEven
.
filter(isEven)
passes isEven
itself as the function. Note that isEven
is a named function.
The same distinction applies to the functions passed to foreach
in the second and third examples.
So, if an expression like 2.0 * 4.0 / 3.0 * 5.0
is actually a series of method calls on Double
s, what are the operator precedence rules? Here they are in order from lowest to highest precedence:
All letters
|
^
&
< >
= !
:
+ -
* / %
All other special characters
Characters on the same line have the same precedence. An exception is =
when it’s used for assignment, in which case it has the lowest precedence.
Because *
and / have the same precedence, the two lines in the following scala
session behave the same:
scala
>
2.0
*
4.0
/
3.0
*
5.0
res0
:
Double
=
13.333333333333332
scala
>
(((
2.0
*
4.0
)
/
3.0
)
*
5.0
)
res1
:
Double
=
13.333333333333332
Usually, method invocation using infix operator notation simply bind in left-to-right order, i.e., they are left-associative. Don’t all methods work this way? No. In Scala, any method with a name that ends with a colon (:
) binds to the right when used in infix notation, while all other methods bind to the left. For example, you can prepend an element to a Seq
using the +:
method (sometimes called “cons,” which is short for “constructor,” a term introduced by Lisp):
scala
>
val
seq
=
Seq
(
'b
'
,
'c
'
,
'd
'
)
val
seq
:
Seq
[
Char
]
=
List
(
b
,
c
,
d
)
scala
>
val
seq2
=
'a
'
+:
seq
val
seq2
:
Seq
[
Char
]
=
List
(
a
,
b
,
c
,
d
)
scala
>
val
seq3
=
'z
'
.+:(
seq2
)
1
|
val
seq3
=
'z
'
.+:(
seq2
)
|
^^^^^^
|
value
+:
is
not
a
member
of
Char
scala
>
val
seq3
=
seq2
.+:(
'z
'
)
val
seq3
:
Seq
[
Char
]
=
List
(
z
,
a
,
b
,
c
,
d
)
Note that if we don’t use infix notation, we have to put seq2
on the left.
While it’s common to declare a type hierarchy to represent all the possible “kinds” of some abstraction, sometimes we know the list of them is fixed and mostly what we need are unique “flags” to indicate each one.
Take for example the days of the week, where we have seven fixed values. An enumeration for the English days of the week can be declared as follows:
enum
WeekDay
(
val
fullName
:
String
)
:
case
Sun
extends
WeekDay
(
"
Sunday
"
)
case
Mon
extends
WeekDay
(
"
Monday
"
)
case
Tue
extends
WeekDay
(
"
Tuesday
"
)
case
Wed
extends
WeekDay
(
"
Wednesday
"
)
case
Thu
extends
WeekDay
(
"
Thursday
"
)
case
Fri
extends
WeekDay
(
"
Friday
"
)
case
Sat
extends
WeekDay
(
"
Saturday
"
)
def
isWorkingDay
:
Boolean
=
!
(
this
==
Sat
||
this
==
Sun
)
import
WeekDay._
val
sorted
=
WeekDay
.
values
.
sortBy
(
_
.
ordinal
)
.
toSeq
assert
(
sorted
==
List
(
Sun
,
Mon
,
Tue
,
Wed
,
Thu
,
Fri
,
Sat
)
)
assert
(
Sun
.
fullName
==
"Sunday"
)
assert
(
Sun
.
ordinal
==
0
)
assert
(
Sun
.
isWorkingDay
==
false
)
assert
(
WeekDay
.
valueOf
(
"Sun"
)
==
WeekDay
.
Sun
)
Declare an enumeration, similar to declaring a class. You can have optional fields as shown. Declare them with val
if you want them to be accessible, e.g., WeekDay.Sun.fullName
. The derives Eql
clause lets use do comparisons with ==
and !=
. We’ll explain this construct in TypeClassDerivation
and MultiversalEquality
.
The values are declared using the case
keyword.
You can define methods.
The WeekDay.values
order does not match the declaration order, so we sort by the ordinal
.
The ordinal value matches the declaration order.
You can lookup a enumeration value by its name.
Scala 3 introduced a new syntax for enumerations, which we saw briefly in SealedClassHierarchies
.1 The new syntax lends itself to a more concise definition of algebraic data types (ADTs - not to be confused with abstract data types). An ADT is ``algebraic` in the sense that transformations obey well defined properties (think of addition with integers as an example), such as transforming an element or combining two of them with an operation can only yield another element in the set.
Consider the following example:
// src/script/scala/progscala3/rounding/TreeADT.scala
object
Scala2ADT
:
sealed
trait
Tree
[
T
]
final
case
class
Branch
[
T
]
(
left
:
Tree
[
T
]
,
right
:
Tree
[
T
]
)
extends
Tree
[
T
]
final
case
class
Leaf
[
T
]
(
elem
:
T
)
extends
Tree
[
T
]
val
tree
=
Branch
(
Branch
(
Leaf
(
1
)
,
Leaf
(
2
)
)
,
Branch
(
Leaf
(
3
)
,
Branch
(
Leaf
(
4
)
,
Leaf
(
5
)
)
)
)
object
Scala3ADT
:
enum
Tree
[
T
]
:
case
Branch
(
left
:
Tree
[
T
]
,
right
:
Tree
[
T
]
)
case
Leaf
(
elem
:
T
)
import
Tree._
val
tree
=
Branch
(
Branch
(
Leaf
(
1
)
,
Leaf
(
2
)
)
,
Branch
(
Leaf
(
3
)
,
Branch
(
Leaf
(
4
)
,
Leaf
(
5
)
)
)
)
Scala2ADT
.
tree
Scala3ADT
.
tree
Use a sealed type hierarchy. Valid for Scala 2 and 3.
One subtype, a branch with left and right children.
The other subtype, a leaf node.
Scala 3 syntax using the new enum
construct. It is much more concise.
The elements of the enum need to be imported.
Is the output the same for these two lines?
We saw sealed type hierarchies before in SealedClassHierarchies
. The enum
syntax provides the same benefits as sealed type hierarchies, but with much less code.
The types of the tree
values are slightly different:
scala
>
Scala2ADT
.
tree
val
res1
:
Scala2ADT.Branch
[
Int
]
=
Branch
(...)
scala
>
Scala3ADT
.
tree
val
res2
:
Scala3ADT.Tree
[
Int
]
=
Branch
(...)
One last point; you may have noticed that Branch
and Leaf
don’t extend Tree
, while in WeekDay
above, each day extends WeekDay
. For Branch
and Leaf
, extending Tree
is inferred by the compiler, although we could add this explicitly. For WeekDay
, each day must extend WeekDay
to provide a value for the val fullName: String
field declared by WeekDay
.
We introduced interpolated strings in ASampleApplication
. Let’s explore them further.
A String
of the form s"foo ${bar}"
will have the value of expression bar
, converted to a String
and inserted in place of ${bar}
. If the expression bar
returns an instance of a type other than String
, a toString
method will be invoked, if one exists. It is an error if it can’t be converted to a String
.
If bar
is just a variable reference, the curly braces can be omitted. For example:
val
name
=
"Buck Trends"
println
(
s"Hello,
$name
"
)
When using interpolated strings, to write a literal dollar sign $
, use two of them, $$
.
There are two other kinds of interpolated strings. The first kind provides printf formatting and uses the prefix f
. The second kind is called “raw” interpolated strings. It doesn’t expand escape characters, like
.
Suppose we’re generating financial reports and we want to show floating-point numbers to two decimal places. Here’s an example:
val
gross
=
100000
F
val
net
=
64000
F
val
percent
=
(
net
/
gross
)
*
100
println
(
f"$$
${
gross
}
%.2f vs. $$
${
net
}
%.2f or
${
percent
}
%.1f%%"
)
The output of the last line is the following:
$100000
.
00
vs
.
$64000
.
00
or
64.0
%
Scala uses Java’s Formatter
class for printf
formatting. The embedded references to expressions use the same ${…}
syntax as before, but printf
formatting directives trail them with no spaces.
In this example, we use two dollar signs, $$, to print a literal US dollar sign, and two percent signs, %%
, to print a literal percent sign. The expression ${gross}%.2f
formats the value of gross
as a floating-point number with two digits after the decimal point.
The types of the variables used must match the format expressions, but some implicit conversions are performed. An Int
expression in a floating point context is allowed. It just pads with zeros. However, attempting to use Double
or Float
in an Int
context causes a compilation error, due to the truncation that would be required.
While Scala uses Java strings, in certain contexts the Scala compiler will wrap a Java String
with extra methods defined in
scala.collection.StringOps
. One of those extra methods is an instance method called format
. You call it on the format string itself, then pass as arguments the values to be incorporated into the string. For example:
scala
>
val
s
=
"%02d: name = %s"
.
format
(
5
,
"Dean Wampler"
)
val
s
:
String
=
"05: name = Dean Wampler"
In this example, we asked for a two-digit integer, padded with leading zeros.
The final version of the built-in string interpolation capabilities is the “raw” format that doesn’t expand control characters. Consider these examples:
scala
>
val
name
=
"Dean Wampler"
val
name
:
String
=
"Dean Wampler"
scala
>
val
multiLine
=
s"
123
$name
456
"
val
multiLine
:
String
=
123
Dean
Wampler
456
scala
>
val
multiLineRaw
=
raw"123 $name 456"
val
multiLineRaw
:
String
=
123
nDean
Wampler
n456
Finally, we can actually define our own string interpolators, but we’ll need to learn more about context abstractions first. See BuildYourOwnStringInterpolator
for details.
Scala conditionals start with the if
keyword. They are expressions, meaning they return a value that you can assign to a variable. In many languages, if
conditionals are statements, which can only perform side effect operations.
Scala 2 if
expressions used braces just like Java’s if
statements, but in Scala 3, if
expressions can also use the new conventions discussed in the previous section. A simple example:
// src/script/scala/progscala3/rounding/If.scala
(
0
until
6
)
foreach
{
n
=>
if
n
%
2
==
0
then
println
(
s"
$n
is even"
)
else
if
n
%
3
==
0
then
println
(
s"
$n
is divisible by 3"
)
else
println
(
n
)
}
Recall from NewScala3Syntax
that the then
keyword is required only if you pass the -new-syntax
flag to the compiler or REPL, which we use in the code examples SBT file. However, if you don’t use that flag, you must wrap the predicate expressions, like n%2 == 0
, in parentheses.
The bodies of each clause are so concise, we can write them on the same line as the if
or else
expressions:
// src/script/scala/progscala3/rounding/If2.scala
(
0
until
6
)
foreach
{
n
=>
if
n
%
2
==
0
then
println
(
s"
$n
is even"
)
else
if
n
%
3
==
0
then
println
(
s"
$n
is divisible by 3"
)
else
println
(
n
)
}
Here are the same examples using the curly brace syntax required by Scala 2 and optional for Scala 3:
// src/script/scala-2/progscala3/rounding/If.scala
(
0
until
6
)
foreach
{
n
=>
if
(
n
%
2
==
0
)
{
println
(
s"
$n
is even"
)
}
else
if
(
n
%
3
==
0
)
{
println
(
s"
$n
is divisible by 3"
)
}
else
{
println
(
n
)
}
}
// src/script/scala-2/progscala3/rounding/If2.scala
(
0
until
6
)
foreach
{
n
=>
if
(
n
%
2
==
0
)
println
(
s"
$n
is even"
)
else
if
(
n
%
3
==
0
)
println
(
s"
$n
is divisible by 3"
)
else
println
(
n
)
}
The older syntax can still be used, even with compiler flags for the new syntax, but you can also use flags to require the older syntax, as discussed in NewScala3Syntax
.
What is the type of the returned value if objects of different types are returned by different branches? The type of the returned value will be the so-called least upper bound (LUB) of all the branches, the closest parent type that matches all the potential values from each clause.
In the following example, the LUB is Option[String]
, because the three branches return either Some[String]
or None. The returned sequence is of type IndexedSeq[Option[String]]
:
// src/script/scala/progscala3/rounding/IfTyped.scala
scala
>
val
seq
=
(
0
until
6
)
map
{
n
=>
|
if
n
%
2
==
0
then
Some
(
n
.
toString
)
|
else
None
|
}
val
seq
:
IndexedSeq
[
Option
[
String
]]
=
Vector
(
Some
(
0
),
None
,
Some
(
2
),
...)
Scala uses the same conditional operators as Java. conditional-operators
provides the details.
Operator | Operation | Description |
---|---|---|
|
and |
The values on the left and right of the operator are true. The righthand side is only evaluated if the lefthand side is true. |
|
or |
At least one of the values on the left or right is true. The righthand side is only evaluated if the lefthand side is false. |
|
greater than |
The value on the left is greater than the value on the right. |
|
greater than or equals |
The value on the left is greater than or equal to the value on the right. |
|
less than |
The value on the left is less than the value on the right. |
|
less than or equals |
The value on the left is less than or equal to the value on the right. |
|
equals |
The value on the left is the same as the value on the right. |
|
not equals |
The value on the left is not the same as the value on the right. |
The &&
and ||
operators are “short-circuiting”. They stop evaluating expressions as soon as the answer is known. This is handy when you must work with null
values:
scala
>
val
s
:
String|Null
=
null
val
s
:
String
|
Null
=
null
scala
>
val
okay
=
s
!=
null
&&
s
.
length
>
5
val
okay
:
Boolean
=
false
Calling s.length
would throw a NullPointerException
without the s != null
test first. What happens if you use ||
instead? Also, we don’t us if
here, because we just want to know the Boolean
value of the conditional.
Most of the operators behave as they do in Java and other languages. An exception is the behavior of ==
and its negation, !=
. In Java, ==
compares instance references only. It doesn’t check logical equality, i.e., comparing field values. You must call the equals
method for that purpose. Instead in Scala, ==
and !=
call the equals
method for the left-hand instance. You actually don’t implement equals
yourself very often in Scala, because you mostly only compare case class instances and the compiler generates equals
automatically for case classes! You can override it when you need to, however.
If you need to determine if two instances are identical references, use the eq
method.
See EqualityOfInstances
for more details.
Another familiar control structure that’s particularly feature-rich in Scala is the for
loop, called the for
comprehension. They are expressions, not statements as in Java.
The term comprehension comes from functional programming. It expresses the idea that we are traversing one or more collections of some kind, “comprehending” something new from it, such as another collection. Python list comprehensions are a similar concept.
Let’s start with a basic for
expression. As for if
expressions, I use the new format options consistently in the code examples, except where noted.
// src/script/scala/progscala3/rounding/BasicFor.scala
for
i
<-
0
until
10
do
println
(
i
)
Since there is one expression inside the for … do
, you can put the expression on the same line after the for
and you can even put everything on one line:
for
i
<-
0
until
10
do
println
(
i
)
for
i
<-
0
until
10
do
println
(
i
)
As you might guess, this code says, “For every integer between 0 inclusive and 10 exclusive, print it on a new line.”
Because this form doesn’t return anything, it only performs side effects. These kinds of for
comprehensions are sometimes called for
loops, analogous to Java for
loops.
In the older Scala 2 syntax, this example would be written as follows:
// src/script/scala-2/progscala3/rounding/BasicFor.scala
for
(
i
<-
0
until
10
)
println
(
i
)
for
(
i
<-
0
until
10
)
println
(
i
)
From now on, in the examples that follow, I’ll only show Scala 3 syntax, but you can find Scala 2 versions of some examples, in the code examples under the directory src/*/scala-2/progscala3/...
and a table of differences in OldVsNewSyntax
.
We can add if
expressions, called guards, to filter for just elements we want to keep:
// src/script/scala/progscala3/rounding/GuardFor.scala
for
n
<-
0
to
6
if
n
%
2
==
0
do
println
(
n
)
The output is the number 0, 2, 4, and 6 (because we use to
to make the 6
inclusive). Note the sense of filtering; the guards express what to keep, not remove.
So far our for
loops have only performed side effects, writing to output. Usually, we want to return a new collection, making our for
expressions comprehensions rather than loops. We use the yield
keyword to express this intent:
// src/script/scala/progscala3/rounding/YieldingFor.scala
val
evens
=
for
n
<-
0
to
10
// Note: 0 to 10, inclusive
if
n
%
2
==
0
yield
n
assert
(
evens
==
Seq
(
0
,
2
,
4
,
6
,
8
,
10
))
Each iteration through the for
expression “yields” a new value, named n
. These are accumulated into a new collection that is assigned to the variable evens
.
The type of the collection resulting from a comprehension expression is inferred from the type of the collection being iterated over. Seq
is a trait and the actual concrete instance returned is of type IndexedSeq
.
In the following example, a Vector[Int]
is converted to a Vector[String]
:
// src/script/scala/progscala3/rounding/YieldingForVector.scala
val
odds
=
for
number
<-
Vector
(
1
,
2
,
3
,
4
,
5
)
if
number
%
2
==
1
yield
number
.
toString
assert
(
odds
==
Vector
(
"1"
,
"3"
,
"5"
))
You can define immutable values inside the for
expressions without using the val
keyword, like fn
in the following example that uses the WeekDay
enumeration we defined earlier in this chapter:
// src/script/scala/progscala3/rounding/ScopedFor.scala
import
progscala3.rounding.WeekDay
val
days
=
for
day
<-
WeekDay
.
values
if
day
.
isWorkingDay
fn
=
day
.
fullName
yield
fn
assert
(
days
.
toSeq
==
Seq
(
"Friday"
,
"Monday"
,
"Tuesday"
,
"Wednesday"
,
"Thursday"
))
In this case, the for
comprehension now returns an Array[String]
, because WeekDay.values
returns an Array[WeekDay]
. Because Array
s are Java Array
s and Java doesn’t define a useful equals
method, we convert to a Seq
with toSeq
and perform the assertion check.
Now let’s consider a powerful use of Option
with for
comprehensions. Recall we discussed Option
as a better alternative to using null
. It’s also useful to recognize that Option
is a special kind of collection, limited to zero or one elements. In fact, we can “comprehend” it too:
// src/script/scala/progscala3/rounding/ScopedOptionFor.scala
import
progscala3.rounding.WeekDay
import
progscala3.rounding.WeekDay._
val
dayOptions
=
Seq
(
Some
(
Mon
),
None
,
Some
(
Tue
),
Some
(
Wed
),
None
,
Some
(
Thu
),
Some
(
Fri
),
Some
(
Sat
),
Some
(
Sun
),
None
)
val
goodDays1
=
for
// First pass
dayOpt
<-
dayOptions
day
<-
dayOpt
fn
=
day
.
fullName
yield
fn
assert
(
goodDays1
==
Seq
(
"Monday"
,
"Tuesday"
,
"Wednesday"
,
...))
val
goodDays2
=
for
// second, more concise pass
case
Some
(
day
)
<-
dayOptions
fn
=
day
.
fullName
yield
fn
assert
(
goodDays1
==
Seq
(
"Monday"
,
"Tuesday"
,
"Wednesday"
,
...))
Imagine that we called some services to return days of the week. The services returned Options
, because some of the services couldn’t return anything, so they returned None
. Now we want to remove and ignore the None
values.
In the first expression of the first for
comprehension, each element extracted is an Option
, assigned to dayOpt
. The next line uses the arrow to extract the value in the option and assign it to day
.
But wait! Doesn’t None
throw an exception if you try to extract a value from it? Yes, but the comprehension effectively checks for this case and skips the None
s. It’s as if we added an explicit if dayOpt != None
before the second line.
Hence, we construct a collection with only values from Some
instances.
The second for
comprehension makes this filtering even cleaner and more concise, using pattern matching. The expression case Some(day) <- dayOptions
only succeeds when the instance is a Some
, also skipping the None
values, and it extracts the value into to day
, all in one step.
To recap the difference between using the left arrow (<-
) versus the equals sign (=
), use the arrow when you are iterating through a collection and extracting values. Use the equals sign when you’re assigning a value from another value that doesn’t involve iteration. A limitation is that the first expression in a for
comprehension has to be an extraction/iteration using the left arrow. If you really need to define a value first, put it before the for
comprehension.
When working with loops in many languages, they provide break
and continue
keywords for breaking out of a loop completely or continuing to the next iteration, respectively. Scala doesn’t have either of these keywords, but when writing idiomatic Scala code, they aren’t missed. Use conditional expressions to test if a loop should continue, or make use of recursion. Better yet, filter your collections ahead of time to eliminate complex conditions within your loops.
The while
loop is seldom used. It executes a block of code as long as a condition is true:
// src/script/scala/progscala3/rounding/While.scala
var
count
=
0
while
count
<
10
count
+=
1
println
(
count
)
assert
(
count
==
10
)
Scala 3 dropped the do-while
construct in Scala 2, because it was rarely used. It can be rewritten using while
, although awkwardly:
// src/script/scala/progscala3/rounding/DoWhileAlternative.scala
var
count
=
0
while
count
+=
1
println
(
count
)
count
<
10
do
()
assert
(
count
==
10
)
Through its use of functional constructs and strong typing, Scala encourages a coding style that lessens the need for exceptions and exception handling. But exceptions are still supported. In particular, they are needed when using Java libraries.
Unlike Java, Scala does not have checked exceptions. Java’s checked exceptions are treated as unchecked by Scala. There is also no throws
clause on method declarations. However, there is a @throws
annotation that is useful for Java interoperability. See Annotations
.
Scala uses pattern matching to specify the exceptions to be caught.
The following example implements a common application scenario, resource management. We want to open files and process them in some way. In this case, we’ll just count the lines. However, we must handle a few error scenarios. The file might not exist, perhaps because the user misspelled the filenames. Also, something might go wrong while processing the file. (We’ll trigger an arbitrary failure to test what happens.) We need to ensure that we close all open file handles, whether or not we process the files successfully:
// src/main/scala/progscala3/rounding/TryCatch.scala
package
progscala3.rounding
import
scala.io.Source
import
scala.util.control.NonFatal
/*
*
Usage: scala rounding.TryCatch filename1 filename2 ...
*/
@main
def
TryCatch
(
fileNames
:
String
*
)
=
fileNames
foreach
{
fileName
=>
var
source
:
Option
[
Source
]
=
None
try
source
=
Some
(
Source
.
fromFile
(
fileName
)
)
val
size
=
source
.
get
.
getLines
.
size
println
(
s"
file
$fileName
has
$size
lines
"
)
catch
case
NonFatal
(
ex
)
=>
println
(
s"
Non fatal exception!
$ex
"
)
finally
for
s
<-
source
do
println
(
s"
Closing
$fileName
...
"
)
s
.
close
}
Import scala.io.Source
for reading input and scala.util.control.NonFatal
for matching on “nonfatal” exceptions, i.e., those where it’s reasonable to attempt recovery.
In Scala 3, we don’t need an object to wrap the “main” method. By using the @main
annotation, we can name the method whatever we want and we can specify the number and types of the arguments expected. Here, we just expect zero or more strings.
Declare the source
to be an Option
, so we can tell in the finally
clause if we have an actual instance or not. We use a mutable variable, but it’s hidden inside the implementation and thread safety isn’t a concern here.
Start of try
clause.
Source.fromFile
will throw a java.io.FileNotFoundException
if the file doesn’t exist. Otherwise, wrap the returned Source
instance in a Some
. Calling get
on the next line is safe, because if we’re here, we know we have a Some
.
Catch nonfatal errors. For example, out of memory would be fatal.
Use a for
comprehension to extract the Source
instance from the Some
and close it. If source
is None
, then nothing happens.
Note the catch
clause. Scala uses pattern matches to pick the exceptions you want to catch. This is more compact and more flexible than Java’s use of separate catch
clauses for each exception. In this case, the clause case NonFatal(ex) => …
uses scala.util.control.NonFatal
to match any exception that isn’t considered fatal.
The finally
clause is used to ensure proper resource cleanup in one place. Without it, we would have to repeat the logic at the end of the try
clause and the catch
clause, to ensure our file handles are closed. Here we use a for
comprehension to extract the Source
from the option. If the option is actually a None
, nothing happens; the block with the close
call is not invoked. Note that since this is “main” method, the handles would be cleaned up anyway on exit, but you’ll want to close resources in other contexts.
When resources need to be cleaned up, whether or not the resource is used successfully, put the cleanup logic in a finally
clause.
This program is already compiled by sbt
and we can run it from the sbt
prompt using the runMain
task, which lets us pass arguments. I have elided some output:
>
runMain
progscala3
.
rounding
.
TryCatch
README
.
md
foo
/
bar
file
README
.
md
has
116
lines
Closing
README
.
md
...
Non
fatal
exception
!
java
.
io
.
FileNotFoundException
:
foo/bar
(
...
)
You throw an exception by writing throw new MyBadException(…)
. If your custom exception is a case
class, you can omit the new
.
Automatic resource management is a common pattern. Let’s use a Scala library facility, scala.util.Using
for this purpose.2 Then we’ll actually implement our own version to illustrate some powerful capabilities in Scala and better understand how the library version works.
// src/main/scala/progscala3/rounding/FileSizes.scala
package
progscala3.rounding
import
scala.util.Using
import
scala.io.Source
/** Usage: scala rounding.FileSizes filename1 filename2 ... */
@main
def
FileSizes
(
fileNames
:
String*
)
=
val
sizes
=
fileNames
map
{
fileName
=>
Using
.
resource
(
Source
.
fromFile
(
fileName
))
{
source
=>
source
.
getLines
.
size
}
}
println
(
s"Returned sizes:
${
sizes
.
mkString
(
", "
)
}
"
)
println
(
s"Total size:
${
sizes
.
sum
}
"
)
This simple program also counts the number of lines in the files specified on the command line. However, if a file is not readable or doesn’t exist, an exception is thrown and processing stops. No other results are produced, unlike the previous TryCatch
example, which continues processing the arguments specified.
See the scala.util.Using
documentation for a few other ways this utility can be used. For more sophisticated approaches to error handling, see Nedelcu2020,Alexandru Nedelcu's blog
.
Now let’s implement our own application resource manager to learn a few powerful techniques that Scala provides for us. This implementation will build on the TryCatch
example:
// src/main/scala/progscala3/rounding/TryCatchArm.scala
package
progscala3.rounding
import
scala.language.reflectiveCalls
import
reflect.Selectable.reflectiveSelectable
import
scala.util.control.NonFatal
import
scala.io.Source
object
manage
:
def
apply
[
R
<:
{
def
close
()
:Unit
}
,T
](
resource
:
=>
R
)(
f
:
R
=>
T
)
:
T
=
var
res
:
Option
[
R
]
=
None
try
res
=
Some
(
resource
)
// Only reference "resource" once!!
f
(
res
.
get
)
// Return the T instance
catch
case
NonFatal
(
ex
)
=>
println
(
s"manage.apply(): Non fatal exception!
$ex
"
)
throw
ex
finally
res
match
case
Some
(
resource
)
=>
println
(
s"Closing resource..."
)
res
.
get
.
close
()
case
None
=>
// do nothing
/** Usage: scala rounding.TryCatchARM filename1 filename2 ... */
@main
def
TryCatchARM
(
fileNames
:
String*
)
=
val
sizes
=
fileNames
map
{
fileName
=>
try
val
size
=
manage
(
Source
.
fromFile
(
fileName
))
{
source
=>
source
.
getLines
.
size
}
println
(
s"file
$fileName
has
$size
lines"
)
size
catch
case
NonFatal
(
ex
)
=>
println
(
s"caught
$ex
"
)
0
}
println
(
"Returned sizes: "
+
(
sizes
.
mkString
(
", "
)))
The output will be similar what we saw for TryCatch
.
This is a lovely little bit of separation of concerns, but to implement it, we used a few new power tools.
First, we named our object manage
rather than Manage
. Normally, you follow the convention of using a leading uppercase letter for type names, but in this case we will use manage
like a function. We want client code to look like we’re using a built-in operator, similar to a while
loop. This is another example of Scala’s tools for building little DSLs.
That manage.apply
method declaration is hairy looking. Let’s deconstruct it. Here is the signature again, spread over several lines and annotated:
def
apply
[
R
<:
{
def
close
(
)
:
Unit
}
,
T
]
(
resource
:
=>
R
)
(
f
:
R
=>
T
)
=
{
.
.
.
}
Two new things are shown here. R
is the type of the resource we’ll manage. The <:
means R
is a subclass of something else. In this case, any type used for R
must contain a close():Unit
method. We declare this using a structural type defined with the braces. What would be more intuitive, especially if you are new to structural types, would be for all resources to implement a Closable
interface that defines a close():Unit
method. Then we could say R <: Closable
. Instead, structural types let us use reflection and plug in any type that has a close():Unit
method (like Source
). Reflection has a lot of overhead and structural types are a bit scary, so reflection is another optional feature, like postfix expressions, which we saw earlier. So we add the import statements to tell the compiler we know what we’re doing.
T
will be the type returned by the anonymous function passed in to do work with the resource.
It looks like resource
is a function with an unusual declaration. Actually, resource
is a by-name parameter, which we first encountered in ATasteOfFutures
.
Finally we have a second parameter list containing a function for the work to do with the resource. This function will take the resource as an argument and return a result of type T
.
Recapping point 1, here is how the apply
method declaration would look if we could assume that all resources implement a Closable
abstraction:
object
manage
{
def
apply
[
R
<:
Closable
,T
](
resource
:
=>
R
)(
f
:
R
=>
T
)
=
...
}
The line, res = Some(resource)
, is the only place resource
is evaluated, which is important, because it is a by-name parameter. We learned in ATasteOfFutures
that they are lazily evaluated, only when used, but they are evaluated every time they are referenced, just like a function call would be. The thing we pass as resource
inside TryCatchARM
, Source.fromFile(fileName)
, should only be evaluated once inside apply
to construct the Source
for a file. The code correctly evaluates it once.
So, you have to use by-name parameters carefully, but their virtue is the ability to control when and even if a block of code is evaluated. We’ll see another example shortly where we will evaluate a by-name parameter repeatedly for a good reason.
To recap, it’s as if the res = …
line is actually this:
res
=
Some
(
Source
.
fromFile
(
fileName
))
After constructing res
, it is passed to the work function f
.
See how manage
is used in TryCatchARM
. It looks like a built-in control structure with one parameter list that creates the Source
and a second parameter list that is a block of code that works with the Source
. So, using manage
looks something like a conventional while
statement.
Like most languages, Scala normally uses call-by-value semantics. If we write val source = Source.fromFile(fileName)
, it is evaluated immediately.
Supporting idiomatic code like our use of manage
is the reason that Scala offers by-name parameters, without which we would have to pass an anonymous function that looks ugly:
manage
(()
=>
Source
.
fromFile
(
fileName
))
{
source
=>
Then, within manage.apply
, our reference to resource
would now be a function call:
val
res
=
Some
(
resource
())
Okay, that’s not a terrible burden, but call by name enables a syntax for building our own control structures, like our manage
utility.
Here is another example using a call by name, this time repeatedly evaluating two by-name parameters; an implementation of a while-like loop construct, called continue
:
// src/script/scala/progscala3/rounding/CallByName.scala
import
scala.annotation.tailrec
@tailrec
def
continue
(
conditional
:
=>
Boolean
)
(
body
:
=>
Unit
)
:
Unit
=
if
conditional
then
body
continue
(
conditional
)
(
body
)
var
count
=
0
continue
(
count
<
5
)
{
println
(
s"
at
$count
"
)
count
+=
1
}
assert
(
count
==
5
)
Ensure the implementation is tail recursive.
Define a continue
function that accepts two argument lists. The first list takes a single, by-name parameter that is the conditional. The second list takes a single, by-name value that is the body to be evaluated for each iteration.
Evaluate the condition. If true, evaluate the body and call continue
recursively.
Try it with traditional brace syntax.
It’s important to note that the by-name parameters are evaluated every time they are referenced. So, by-name parameters are in a sense lazy, because evaluation is deferred, but possibly repeated over and over again. Scala also provides lazy values. By the way, this implementation shows how “loop” constructs can be replaced with recursion.
Unfortunately, this ability to define our own control structures only works with the old Scala 2 syntax using parentheses and braces. If continue
really behaved like while
or similar built-in constructs, we would be able to write the last two examples. The syntax uniformity with user-defined constructs is a nice feature we must give up if we use the new syntax… or do we??
count
=
0
continue
(
count
<
5
)
println
(
s"at
$count
"
)
count
+=
1
assert
(
count
==
5
)
This actually parses, but it executes in an unexpected way. Here it is again, annotated to explain what actually happens:
continue
(
count
<
5
)
// Returns a "partially-applied function"
println
(
s"at
$count
"
)
// A separate statement that prints "at 0" once
count
+=
1
// Increments count once
A partially-applied function is one where we provide some, but not all arguments, returning a new function that will accept the remaining arguments (see PartiallyAppliedFunctionsVsPartialFunctions
). Here’s what the REPL will print for that line:
scala
>
continue
(
count
<
5
)
val
res1
:
(=>
Unit
)
=>
Unit
=
Lambda$11103
/
0x00000008048c0040
@
4
d0f9ec0
This is an anonymous function, implemented with a JDK lambda on the JDK, that takes a by-name parameter returning Unit
(recall the second parameter for continue
) and then returns Unit
.
The second and third lines are not treated as part of the body
that should be passed to continue
. They are treated as single statements that are evaluated once.
So, the optional braces don’t work.
By-name parameters show us that lazy evaluation is useful, but they are evaluated every time they are referenced.
There are times when you want to evaluate an expression once to initialize a field in an object, but you want to defer that invocation until the value is actually needed. In other words, on-demand evaluation. This is useful when:
The expression is expensive (e.g., opening a database connection) and you want to avoid the overhead until the value is actually needed, which could be never.
You want to improve startup times for modules by deferring work that isn’t needed immediately.
A field in an object needs to be initialized lazily so that other initializations can happen first.
We’ll explore the last scenario when we discuss InitializingFields
.
Here is a “sketch” of an example using a lazy val
:
// src/script/scala/progscala3/rounding/LazyInitVal.scala
case
class
DBConnection
()
:
println
(
"
In
constructor
"
)
type
MySQLConnection
=
String
lazy
val
connection
:
MySQLConnection
=
// Connect to the database
println
(
"Connected!"
)
"DB"
The lazy
keyword indicates that evaluation will be deferred until the value is accessed.
Let’s try it. Notice when the println
statements are executed:
scala
>
val
dbc
=
DBConnection
()
In
constructor
val
dbc
:
DBConnection
=
DBConnection
()
scala
>
dbc
.
connection
Connected
!
val
res4
:
dbc.MySQLConnection
=
DB
scala
>
dbc
.
connection
val
res5
:
dbc.MySQLConnection
=
DB
So, how is a lazy val
different from a method call? We see that “Connected!” was only printed once, whereas if connection
were a method, the body would be executed every time and we would see “Connected!” printed each time. Furthermore, we didn’t see that message until when we referenced connection
the first time.
One-time evaluation makes little sense for a mutable field. Therefore, the lazy
keyword is not allowed on var
s.
Lazy values are implemented with the equivalent of a guard. When client code references a lazy value, the reference is intercepted by the guard to check if initialization is required. This guard step is really only essential the first time the value is referenced, so that the value is initialized first before the access is allowed to proceed. Unfortunately, there is no easy way to eliminate these checks for subsequent calls. So, lazy values incur overhead that “eager” values don’t. Therefore, you should only use lazy values when initialization is expensive, especially if the value may not actually be used. There are also some circumstances where careful ordering of initialization dependencies is most easily implemented by making some values lazy (see InitializingFields
).
There is a @threadUnsafe
annotation you can add to a lazy val. It causes the initialization to use a faster mechanism which is not thread-safe, so use with caution.
Until now, I have emphasized the power of functional programming in Scala. I waited until now to discuss Scala’s features for object-oriented programming, such as how abstractions and concrete implementations are defined, and how inheritance is supported. We’ve seen some details in passing, like abstract and case classes and objects, but now it’s time to cover these concepts.
Scala uses traits to define abstractions. We’ll explore most details in Traits
, but for now, think of them as interfaces for declaring abstract member fields, methods, and types, with the option of defining any or all of them, too.
Traits enable true separation of concerns and composition of behaviors (“mixins”).
Here is a typical enterprise developer task, adding logging. Let’s start with a service:
// src/script/scala/progscala3/rounding/Traits.scala
import
util.Random
class
Service
(
name
:
String
)
:
def
work
(
i:
Int
)
:
(
Int
,
Int
)
=
(
i
,
Random
.
between
(
0
,
1000
))
val
service1
=
new
Service
(
"one"
)
(
1
to
3
)
foreach
(
i
=>
println
(
s"Result:
${
service1
.
work
(
i
)
}
"
))
We ask the service to do some (random) work and get this output:
Result: (1,975) Result: (2,286) Result: (3,453)
Now we want to mix in a standard logging library. For simplicity, we’ll just use println
.
Here are two traits, one that defines the abstraction with no concrete members and the other that implements the abstraction for “logging” to standard output:
trait
Logging
:
def
info
(
message:
String
)
:
Unit
def
warning
(
message
:
String
)
:
Unit
def
error
(
message
:
String
)
:
Unit
trait
StdoutLogging
extends
Logging
:
def
info
(
message:
String
)
=
println
(
s"INFO:
$message
"
)
def
warning
(
message
:
String
)
=
println
(
s"WARNING:
$message
"
)
def
error
(
message
:
String
)
=
println
(
s"ERROR:
$message
"
)
Note that Logging
is pure abstract. It works exactly like a Java interface. It is even implemented the same way in JVM byte code.
Finally, let’s declare a service that “mixes in” logging and use it:
val
service2
=
new
Service
(
"two"
)
with
StdoutLogging
:
override
def
work
(
i:
Int
)
:
(
Int
,
Int
)
=
info
(
s"Starting work: i =
$i
"
)
val
result
=
super
.
work
(
i
)
info
(
s"Ending work: result =
$result
"
)
result
(
1
to
3
)
foreach
(
i
=>
println
(
s"Result:
${
service2
.
work
(
i
)
}
"
))
We override the work
method to log when we enter and before we leave the method. Scala requires the override
keyword when you override a concrete method in a parent class. This prevents mistakes when you didn’t know you were overriding a method, for example from a library parent class and it catches misspelled method names that aren’t actually overrides! Note how we access the parent class work
method, using super.work
.
Here is the output:
INFO: Starting work: i = 1 INFO: Ending work: result = (1,737) Result: (1,737) INFO: Starting work: i = 2 INFO: Ending work: result = (2,310) Result: (2,310) INFO: Starting work: i = 3 INFO: Ending work: result = (3,273) Result: (3,273)
Be very careful about overriding concrete methods! In this case, we don’t change the behavior of the the parent-class method. We just log activity, then call the parent method, then log again. We are careful to return the result unchanged that was returned by the parent method.
To mix in traits while constructing an instance as shown, we use the with
keyword. We can mix in as many as we want. Some traits might not modify existing behavior at all, and just add new useful, but independent methods.
In this example, we’re actually modifying the behavior of work
, in order to inject logging, but we are not changing its “contract” with clients, that is, its external behavior.3
If we needed multiple instances of Service with StdoutLogging
, we should declare a class:
class
LoggedService
(
name
:
String
)
extends
Service
(
name
)
with
StdoutLogging
:
...
Note how we pass the name
argument to the parent class Service
. To create instances, new LoggedService("three")
works as you would expect it to work.
There is a lot more to discuss about traits and mixin composition, as we’ll see.
We’ve covered a lot of ground in these first chapters. We learned how flexible and concise Scala code can be. In this chapter, we learned some powerful constructs for defining DSLs and for manipulating data, such as for
comprehensions. Finally, we learned more about enumerations and the basic capabilities of traits
.
You should now be able to read quite a lot of Scala code, but there’s plenty more about the language to learn. Next we’ll begin a deeper dive into Scala features.
1 You can find a Scala 2 version of WeekDay
in the code examples, src/script/scala-2/progscala3/rounding/WeekDay.scala
.
2 Not to be confused with the keyword using
that we discussed in ATasteOfFutures
.
3 That’s not strictly true, in the sense that the extra I/O has changed the code’s interaction with the “world.”
18.217.7.174