3.1 Values | 39 |
3.2 Loops, Conditionals, Comprehensions | 46 |
3.3 Methods and Functions | 50 |
3.4 Classes and Traits | 53 |
for
(
i <-
Range.
inclusive(
1
,
100
)
)
{
println(
if
(
i %
3
==
0
&&
i %
5
==
0
)
"FizzBuzz"
else
if
(
i %
3
==
0
)
"Fizz"
else
if
(
i %
5
==
0
)
"Buzz"
else
i
)
}
</> 3.1.scala
Snippet 3.1: the popular "FizzBuzz" programming challenge, implemented in Scala
This chapter is a quick tour of the Scala language. For now we will focus on the basics of Scala that are similar to what you might find in any mainstream programming language.
The goal of this chapter is to get familiar you enough that you can take the same sort of code you are used to writing in some other language and write it in Scala without difficulty. This chapter will not cover more Scala-specific programming styles or language features: those will be left for Chapter 5: Notable Scala Features.
For this chapter, we will write our code in the Ammonite Scala REPL:
$ amm
Loading...
Welcome to the Ammonite Repl 2.2.0 (Scala 2.13.2 Java 11.0.7)
@
</> 3.2.bash
Scala has the following sets of primitive types:
|
|
These types are identical to the primitive types in Java, and would be similar
to those in C#, C++, or any other statically typed programming language. Each
type supports the typical operations, e.g. booleans support boolean logic ||
, numbers support arithmetic &&
+
-
*
and bitwise operations /
|
,
and so on. All values support &
to check for equality and ==
to check for
inequality.!=
Numbers default to 32-bit
s. Precedence for arithmetic operations follows
other programming languages: Int
and *
have higher precedence than /
or +
,
and parentheses can be used for grouping.-
@ 1
+
2
*
3
res0:
Int
=
7
@ (
1
+
2
)
*
3
res1:
Int
=
9
</> 3.3.scala
s are signed and wrap-around on overflow, while 64-bit Int
s suffixed
with Long
L
have a bigger range and do not overflow as easily:
|
|
Apart from the basic operators, there are a lot of useful methods on
java
and .
lang.
Integerjava
:.
lang.
Long
@ java.
lang.
Integer.
<
tab>
BYTES decode numberOfTrailingZeros
signum MAX_VALUE divideUnsigned
getInteger parseUnsignedInt toBinaryString
.
.
.
@ java.
lang.
Integer.
toBinaryString(
123
)
res6:
String
=
"1111011"
@ java.
lang.
Integer.
numberOfTrailingZeros(
24
)
res7:
Int
=
3
</> 3.6.scala
64-bit
s are specified using the Double
syntax, and have a similar set of
arithmetic operations. You can also use the 1.0
syntax to ask for 32-bit
1.0F
s:Float
@ 1.0
/
3.0
res8:
Double
=
0.3333333333333333
@ 1.0F
/
3.0F
res9:
Float
=
0.33333334F
</> 3.7.scala
32-bit
s take up half as much memory as 64-bit Float
s, but are more
prone to rounding errors during arithmetic operations. Double
java
and
.
lang.
Float
java
have a similar set of useful operations you can perform on
.
lang.
Double
s and Float
s.Double
s in Scala are arrays of 16-bit String
s:Char
@ "hello world"
res10:
String
=
"hello world"
</> 3.8.scala
s can be sliced with String
, constructed via
concatenation using .
substring
, or via string interpolation by prefixing the literal
with +
s"..."
and interpolating the values with $
or $
:{
.
.
.
}
|
|
You can name local values with the
keyword:val
@ val
x =
1
@ x +
2
res18:
Int
=
3
</> 3.11.scala
Note that
s are immutable: you cannot re-assign the val
to a different
value after the fact. If you want a local variable that can be re-assigned, you
must use the val
x
keyword.var
|
|
In general, you should try to use
where possible: most named values
in your program likely do not need to be re-assigned, and using val
helps prevent mistakes where you re-assign something accidentally. Use val
only if you are sure you will need to re-assign something later.var
Both
s and val
s can be annotated with explicit types. These can serve as
documentation for people reading your code, as well as a way to catch errors if
you accidentally assign the wrong type of value to a variablevar
|
|
Tuples are fixed-length collections of values, which may be of different types:
@ val
t =
(
1
,
true
,
"hello"
)
t:
(
Int
,
Boolean
,
String
)
=
(
1
,
true
,
"hello"
)
@ t.
_1
res27:
Int
=
1
@ t.
_2
res28:
Boolean
=
true
@ t.
_3
res29:
String
=
"hello"
</> 3.16.scala
Above, we are storing a tuple into the local value t
using the
syntax, and then using (
a,
b,
c)
, .
_1
and .
_2
to extract the values out of it.
The fields in a tuple are immutable..
_3
The type of the local value t
can be annotated as a tuple type:
@ val
t:
(
Int
,
Boolean
,
String
)
=
(
1
,
true
,
"hello"
)
You can also use the
syntax to extract all the values at
once, and assign them to meaningful names:val
(
a,
b,
c)
=
t
|
|
Tuples come in any size from 1 to 22 items long:
@ val
t =
(
1
,
true
,
"hello"
,
'c'
,
0.2
,
0.5f
,
12345678912345L
)
t:
(
Int
,
Boolean
,
String
,
Char
,
Double
,
Float
,
Long
)
=
(
1
,
true
,
"hello"
,
'c'
,
0.2
,
0.5F
,
12345678912345L
)
</> 3.19.scala
Most tuples should be relatively small. Large tuples can easily get confusing:
while working with .
_1
and .
_2
is probably fine, when you end up
working with .
_3.
_11
it becomes easy to mix up the different fields. If
you find yourself working with large tuples, consider defining a
Class (3.4) or Case Class that we will see in Chapter 5: Notable Scala Features..
_13
Arrays are instantiated using the Array
syntax, and entries within
each array are retrieved using [
T]
(
a,
b,
c)
a
:(
n)
@ val
a =
Array[
Int
]
(
1
,
2
,
3
,
4
)
@ a(
0
)
// first entry, array indices start from 0
res36:
Int
=
1
@ a(
3
)
// last entry
res37:
Int
=
4
@ val
a2 =
Array[
String
]
(
"one"
,
"two"
,
"three"
,
"four"
)
a2:
Array[
String
]
=
Array(
"one"
,
"two"
,
"three"
,
"four"
)
@ a2(
1
)
// second entry
res39:
String
=
"two"
</> 3.20.scala
The type parameter inside the square brackets
or [
Int
]
determines
the type of the array, while the parameters inside the parenthesis [
String
]
determine its initial contents. Note that looking up an Array by index is
done via parentheses (
1
,
2
,
3
,
4
)
a
rather than square brackets (
3
)
a
as is common in
many other programming languages.[
3
]
You can omit the explicit type parameter and let the compiler infer the Array's
type, or create an empty array of a specified type using
,
and assign values to each index later:new
Array[
T]
(
length)
|
|
For Array
s created using
, all entries start off with the value new
Array
for numeric arrays, 0
for false
arrays, and Boolean
for null
s and
other types. String
Array
s are mutable but fixed-length: you can change the value of
each entry but cannot change the number of entries by adding or removing values.
We will see how to create variable-length collections later in Chapter 4: Scala Collections.
Multi-dimensional arrays, or arrays-of-arrays, are also supported:
@ val
multi =
Array(
Array(
1
,
2
)
,
Array(
3
,
4
)
)
multi:
Array[
Array[
Int
]
]
=
Array(
Array(
1
,
2
)
,
Array(
3
,
4
)
)
@ multi(
0
)
(
0
)
res47:
Int
=
1
@ multi(
0
)
(
1
)
res48:
Int
=
2
@ multi(
1
)
(
0
)
res49:
Int
=
3
@ multi(
1
)
(
1
)
res50:
Int
=
4
</> 3.23.scala
Multi-dimensional arrays can be useful to represent grids, matrices, and similar values.
Scala's Option
type allows you to represent a value that may or may not
exist. An [
T]
Option
can either be [
T]
Some
indicating that a value is
present, or (
v:
T)
None
indicating that it is absent:
@ def
hello(
title:
String
,
firstName:
String
,
lastNameOpt:
Option[
String
]
)
=
{
lastNameOpt match
{
case
Some(
lastName)
=>
println(
s"Hello $title. $lastName")
case
None =>
println(
s"Hello $firstName")
}
}
@ hello(
"Mr"
,
"Haoyi"
,
None)
Hello Haoyi
@ hello(
"Mr"
,
"Haoyi"
,
Some(
"Li"
)
)
Hello Mr.
Li
</> 3.24.scala
The above example shows you how to construct Option
s using Some
and None
,
as well as
ing on them in the same way. Many APIs in Scala rely on
match
Option
s rather than
s for values that may or may not exist. In general,
null
Option
s force you to handle both cases of present/absent, whereas when using
s it is easy to forget whether or not a value is null-able, resulting in
confusing null
NullPointerException
s at runtime. We will go deeper into pattern
matching in Chapter 5: Notable Scala Features.
Option
s contain some helper methods that make it easy to work with the
optional value, such as getOrElse
, which substitutes an alternate value if the
Option
is None
:
@ Some(
"Li"
)
.
getOrElse(
"<unknown>"
)
res54:
String
=
"Li"
@ None.
getOrElse(
"<unknown>"
)
res55:
String
=
"<unknown>"
</> 3.25.scala
Option
s are very similar to a collection whose size is
or 0
. You can
loop over them like normal collections, or transform them with standard
collection operations like 1
..
map
|
|
Above, we combine
and .
map
to print out the length of the name if
present, and otherwise print .
getOrElse
. We will learn more about collection
operations in Chapter 4: Scala Collections.-
1
For-loops in Scala are similar to "foreach" loops in other languages: they
directly loop over the elements in a collection, without needing to explicitly
maintain and increment an index. If you want to loop over a range of indices,
you can loop over a Range
such as Range
:(
0
,
5
)
|
|
You can loop over nested Array
s by placing multiple
s in the header of the
loop:<-
@ val
multi =
Array(
Array(
1
,
2
,
3
)
,
Array(
4
,
5
,
6
)
)
@ for
(
arr <-
multi;
i <-
arr)
println(
i)
1
2
3
4
5
6
</> 3.30.scala
Loops can have guards using an
syntax:if
@ for
(
arr <-
multi;
i <-
arr;
if
i %
2
==
0
)
println(
i)
2
4
6
</> 3.31.scala
-if
conditionals are similar to those in any other programming language.
One thing to note is that in Scala else
-if
can also be used as an
expression, similar to the else
a
ternary expressions in other languages.
Scala does not have a separate ternary expression syntax, and so the ?
b :
c
-if
can be directly used as the right-hand-side of the else
total
below.+=
|
|
Now that we know the basics of Scala syntax, let's consider the common "Fizzbuzz" programming challenge:
Write a short program that prints each number from 1 to 100 on a new line.
For each multiple of 3, print "Fizz" instead of the number.
For each multiple of 5, print "Buzz" instead of the number.
For numbers which are multiples of both 3 and 5, print "FizzBuzz" instead of the number.
We can accomplish this as follows:
@ for
(
i <-
Range.
inclusive(
1
,
100
)
)
{
if
(
i %
3
==
0
&&
i %
5
==
0
)
println(
"FizzBuzz"
)
else
if
(
i %
3
==
0
)
println(
"Fizz"
)
else
if
(
i %
5
==
0
)
println(
"Buzz"
)
else
println(
i)
}
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
.
.
.
</> 3.34.scala
Since
-if
is an expression, we can also write it as:else
@ for
(
i <-
Range.
inclusive(
1
,
100
)
)
{
println(
if
(
i %
3
==
0
&&
i %
5
==
0
)
"FizzBuzz"
else
if
(
i %
3
==
0
)
"Fizz"
else
if
(
i %
5
==
0
)
"Buzz"
else
i
)
}
</> 3.35.scala
Apart from using
to define loops that perform some action, you can also
use for
together with for
to transform a collection into a new collection:yield
@ val
a =
Array(
1
,
2
,
3
,
4
)
@ val
a2 =
for
(
i <-
a)
yield
i *
i
a2:
Array[
Int
]
=
Array(
1
,
4
,
9
,
16
)
@ val
a3 =
for
(
i <-
a)
yield
"hello "
+
i
a3:
Array[
String
]
=
Array(
"hello 1"
,
"hello 2"
,
"hello 3"
,
"hello 4"
)
</> 3.36.scala
Similar to loops, you can filter which items end up in the final collection
using an
guard inside the parentheses:if
@ val
a4 =
for
(
i <-
a if
i %
2
==
0
)
yield
"hello "
+
i
a4:
Array[
String
]
=
Array(
"hello 2"
,
"hello 4"
)
</> 3.37.scala
Comprehensions can also take multiple input arrays, a
and b
below. This
flattens them out into one final output Array
, similar to using a nested
for-loop:
@ val
a =
Array(
1
,
2
)
;
val
b =
Array(
"hello"
,
"world"
)
@ val
flattened =
for
(
i <-
a;
s <-
b)
yield
s +
i
flattened:
Array[
String
]
=
Array(
"hello1"
,
"world1"
,
"hello2"
,
"world2"
)
</> 3.38.scala
You can also replace the parentheses
with curly brackets (
)
if you wish
to spread out the nested loops over multiple lines, for easier reading. Note
that the order of {
}
s within the nested comprehension matters, just like how
the order of nested loops affects the order in which the loop actions will take
place:<-
@ val
flattened =
for
{
i <-
a
s <-
b
}
yield
s +
i
flattened:
Array[
String
]
=
Array(
"hello1"
,
"world1"
,
"hello2"
,
"world2"
)
@ val
flattened2 =
for
{
s <-
b
i <-
a
}
yield
s +
i
flattened2:
Array[
String
]
=
Array(
"hello1"
,
"hello2"
,
"world1"
,
"world2"
)
</> 3.39.scala
We can use comprehensions to write a version of FizzBuzz that doesn't print its
results immediately to the console, but returns them as a Seq
(short for
"sequence"):
@ val
fizzbuzz =
for
(
i <-
Range.
inclusive(
1
,
100
)
)
yield
{
if
(
i %
3
==
0
&&
i %
5
==
0
)
"FizzBuzz"
else
if
(
i %
3
==
0
)
"Fizz"
else
if
(
i %
5
==
0
)
"Buzz"
else
i.
toString
}
fizzbuzz:
IndexedSeq[
String
]
=
Vector(
"1"
,
"2"
,
"Fizz"
,
"4"
,
"Buzz"
,
.
.
.
</> 3.40.scala
We can then use the fizzbuzz
collection however we like: storing it in a
variable, passing it into methods, or processing it in other ways. We will cover
what you can do with these collections later, in Chapter 4: Scala Collections.
You can define methods using the
keyword:def
@ def
printHello(
times:
Int
)
=
{
println(
"hello "
+
times)
}
@ printHello(
1
)
hello 1
@ printHello(
times =
2
)
// argument name provided explicitly
hello 2
</> 3.41.scala
Passing in the wrong type of argument, or missing required arguments, is a compiler error. However, if the argument has a default value, then passing it is optional.
|
|
Apart from performing actions like printing, methods can also return values. The
last expression within the curly brace
block is treated as the return value
of a Scala method.{
}
@ def
hello(
i:
Int
=
0
)
=
{
"hello "
+
i
}
@ hello(
1
)
res96:
String
=
"hello 1"
</> 3.44.scala
You can call the method and print out or perform other computation on the returned value:
@ println(
hello(
)
)
hello 0
@ val
helloHello =
hello(
123
)
+
" "
+
hello(
456
)
helloHello:
String
=
"hello 123 hello 456"
@ helloHello.
reverse
res99:
String
=
"654 olleh 321 olleh"
</> 3.45.scala
You can define function values using the
syntax. Functions values are
similar to methods, in that you call them with arguments and they can perform
some action or return some value. Unlike methods, functions themselves are
values: you can pass them around, store them in variables, and call them later.=>
@ var
g:
Int
=>
Int
=
i =>
i +
1
@ g(
10
)
res101:
Int
=
11
@ g =
i =>
i *
2
@ g(
10
)
res103:
Int
=
20
</> 3.46.scala
Note that unlike methods, function values cannot have optional arguments (i.e.
with default values) and cannot take type parameters via the
syntax. When
a method is converted into a function value, any optional arguments must be
explicitly included, and type parameters fixed to concrete types. Function
values are also anonymous, which makes stack traces involving them less
convenient to read than those using methods.[
T]
In general, you should prefer using methods unless you really need the flexibility to pass as parameters or store them in variables. But if you need that flexibility, function values are a great tool to have.
One common use case of function values is to pass them into methods that take
function parameters. Such methods are often called "higher order methods".
Below, we have a class Box
with a method printMsg
that prints its contents
(an
), and a separate method Int
update
that takes a function of type
that can be used to update Int
=>
Int
x
. You can then pass a function literal into
update
in order to change the value of x
:
|
|
Simple functions literals like i
can also be written via the
shorthand =>
i +
5
_
, with the underscore +
5
_
standing in for the function
parameter.
@ b.
update(
_ +
5
)
@ b.
printMsg(
"Hello"
)
Hello11
</> 3.49.scala
This placeholder syntax for function literals also works for multi-argument
functions, e.g.
can be written as (
x,
y)
=>
x +
y_
.+
_
Any method that takes a function as an argument can also be given a method
reference, as long as the method's signature matches that of the function type,
here
:Int
=>
Int
@ def
increment(
i:
Int
)
=
i +
1
@ val
b =
new
Box(
123
)
@ b.
update(
increment)
// Providing a method reference
@ b.
update(
x =>
increment(
x)
)
// Explicitly writing out the function literal
@ b.
update{
x =>
increment(
x)
}
// Methods taking a single function can be called with {}s
@ b.
update(
increment(
_)
)
// You can also use the `_` placeholder syntax
@ b.
printMsg(
"result: "
)
result:
127
</> 3.50.scala
Methods can be defined to take multiple parameter lists. This is useful for
writing higher-order methods that can be used like control structures, such as
the myLoop
method below:
|
|
The ability to pass function literals to methods is used to great effect in the standard library, to concisely perform transformations on collections. We will see more of that in Chapter 4: Scala Collections.
You can define classes using the
keyword, and instantiate them using
class
. By default, all arguments passed into the class constructor are available
in all of the class' methods: the new
above defines both the private
fields as well as the class' constructor. (
x:
Int
)
x
is thus accessible in the
printMsg
function, but cannot be accessed outside the class:
|
|
To make x
publicly accessible you can make it a
, and to make it mutable
you can make it a val
:var
|
|
|
|
You can also use
s or val
s in the body of a class to store data. These
get computed once when the class is instantiated:var
|
|
s are similar to trait
interface
s in traditional object-oriented languages: a
set of methods that multiple classes can inherit. Instances of these classes can
then be used interchangeably.
@ trait
Point{
def
hypotenuse:
Double
}
@ class
Point2D(
x:
Double
,
y:
Double
)
extends
Point{
def
hypotenuse =
math.
sqrt(
x *
x +
y *
y)
}
@ class
Point3D(
x:
Double
,
y:
Double
,
z:
Double
)
extends
Point{
def
hypotenuse =
math.
sqrt(
x *
x +
y *
y +
z *
z)
}
@ val
points:
Array[
Point]
=
Array(
new
Point2D(
1
,
2
)
,
new
Point3D(
4
,
5
,
6
)
)
@ for
(
p <-
points)
println(
p.
hypotenuse)
2.23606797749979
8.774964387392123
</> 3.61.scala
Above, we have defined a Point
trait with a single method
. The subclasses def
hypotenuse:
Double
Point2D
and Point3D
both have different sets of
parameters, but they both implement
. Thus we can put both
def
hypotenusePoint2D
s and Point3D
s into our points
and treat them all
uniformly as objects with a :
Array[
Point]
method, regardless of what their
actual class is.def
hypotenuse
In this chapter, we have gone through a lightning tour of the core Scala language. While the exact syntax may be new to you, the concepts should be mostly familiar: primitives, arrays, loops, conditionals, methods, and classes are part of almost every programming language. Next we will look at the core of the Scala standard library: the Scala Collections.
Exercise: Define a
method that takes a def
flexibleFizzBuzz
callback
function as its argument, and allows the caller to decide what they want to do
with the output. The caller can choose to ignore the output, String
=>
Unit
println
the
output directly, or store the output in a previously-allocated array they
already have handy.
|
|
Exercise: Write a recursive method printMessages
that can receive an array of Msg
class instances, each with an optional parent
ID, and use it to print out a
threaded fashion. That means that child messages are print out indented
underneath their parents, and the nesting can be arbitrarily deep.
class
Msg(
val
id:
Int
,
val
parent:
Option[
Int
]
,
val
txt:
String
)
def
printMessages(
messages:
Array[
Msg]
)
:
Unit
=
.
.
.
</> 3.64.scala
|
|
Exercise: Define a pair of methods withFileWriter
and withFileReader
that can be
called as shown below. Each method should take the name of a file, and a
function value that is called with a java
or
.
io.
BufferedReaderjava
that it can use to read or write data. Opening and
closing of the reader/writer should be automatic, such that a caller cannot
forget to close the file. This is similar to Python "context managers" or Java
"try-with-resource" syntax..
io.
BufferedWriter
TestContextManagers.sc
withFileWriter
</> 3.67.scala(
"File.txt"
)
{
writer=>
writer.
write(
"Hello\n"
)
;
writer.
write(
"World!"
)
}
val
result=
withFileReader(
"File.txt"
)
{
reader=>
reader.
readLine(
)
+
"\n"
+
reader.
readLine(
)
}
assert(
result==
"Hello\nWorld!"
)
You can use the Java standard library APIs
java
and .
nio.
file.
Files.
newBufferedWriternewBufferedReader
for working
with file readers and writers. We will get more familiar with working with
files and the filesystem in Chapter 7: Files and Subprocesses.