Topics in This Chapter A1
In this chapter, you will learn how to implement conditions, loops, and functions in Scala. You will encounter a fundamental difference between Scala and other programming languages. In Java or C++, we differentiate between expressions (such as 3 + 4
) and statements (for example, an if
statement). An expression has a value; a statement carries out an action. In Scala, almost all constructs have values. This feature can make programs more concise and easier to read.
Here are the highlights of this chapter:
An if
expression has a value.
A block has a value—the value of its last expression.
The Scala for
loop is like an “enhanced” Java for
loop.
Semicolons are (mostly) optional.
In Scala 3, indentation is preferred over braces.
The void
type is Unit
.
Avoid using return
in a function.
Scala functions can have default, named, and variable arguments.
The program entrypoint is the function annotated with @main
.
Exceptions work just like in Java or C++, but you use a “pattern matching” syntax for catch
.
Scala has no checked exceptions.
Like most programming languages, Scala has an if
/else
construct. In languages such as Java and C++, if
/else
is a statement. It carries out one action or another. However, in Scala, an if
/else
is an expression. It has a value, namely the value of the expression that follows the if
or else
. For example,
if x > 0 then 1 else -1
has a value of 1
or -1
, depending on the value of x
. You can put that value in a variable:
val s = if x > 0 then 1 else -1
Contrast this with
var t = 0
if x > 0 then t = 1 else t = -1
The first form is better because it can be used to initialize a val
. In the second form, t
needs to be a var
.
As already mentioned, semicolons are mostly optional in Scala—see Section 2.2, “Statement Termination,” on page 20.
Note
Scala also supports the C-style syntax if (condition) ...
, but this book uses the if condition then ...
syntax that was introduced in Scala 3.
Java and C++ have a ?:
operator for conditionally selecting among two expressions:
x > 0 ? 1 : -1 //
Java or C++
In Python, you write
1 if x > 0 else -1 #
Python
Both are equivalent to the Scala expression if x > 0 then 1 else -1
. In Scala, you don’t need separate forms for conditional expressions and statements.
In Scala, every expression has a type. For example, the expression if x > 0 then 1 else -1
has the type Int
because both branches have the type Int
. The type of a mixed-type expression, such as
if x > 0 then "positive" else -1
is the common supertype of both branches. In this example, one branch is a java.lang.String
, and the other an Int
. As it happens, these two types have a common supertype Matchable
. In the most extreme case, the expression has as its type the most general of all types, called Any
.
If the else
part is omitted, for example in
if x > 0 then "positive"
it is possible that the if
expression yields no value. However, in Scala, every expression is supposed to have some value. This is finessed by introducing a class Unit
that has one value, written as ()
. The if
without an else
is considered a statement and always has value ()
.
Think of ()
as a placeholder for “no useful value,” and of Unit
as an analog of void
in Java or C++.
(Technically speaking, void
has no value whereas Unit
has one value that signifies “no value.” If you are so inclined, you can ponder the difference between an empty wallet and a wallet with a bill labeled “no dollars.”)
The Scala REPL does not display the ()
value. To see the value in the REPL, print it:
println(if x > 0 then "positive")
You will get a warning when you use an if
expression without an else
. It is usually an error.
However, if the body of the if
has type Unit
, there is no problem:
if x < 0 then println("negative")
The println
method is only called for its side effect—to display a string on the console. It has return type Unit
and always returns ()
. Therefore, the value of the if
expression is always ()
. This usage is correct, and no warning is displayed.
Note
Scala has no switch
statement, but it has a much more powerful pattern matching mechanism that we will discuss in Chapter 14. For now, just use a sequence of if
statements.
Caution
The REPL is more nearsighted than the compiler—it only sees one line of code at a time. For example, consider typing the following code into the REPL, one character at a time:
if x > 0 then 1
else if x == 0 then 0 else -1
As soon as you press the Enter key at the end of the first line, the REPL executes if x > 0 then 1
and shows the answer. (The answer is ()
, which confusingly is not displayed, followed by a warning that an if
without else
is a statement.) Then an error is reported when you press Enter after the second line since an else
without an if
is illegal.
To avoid this issue, put the else
on the same line so that the REPL knows that more code is coming:
if x > 0 then 1 else
if x == 0 then 0 else -1
This is only a concern in the REPL. In a compiled program, the parser will find the else
on the next line.
Note
If you copy a block of code from a text editor or a web page and paste it into the REPL, then this problem doesn’t occur. The REPL analyzes a pasted code snippet in its entirety.
In Java and C++, every statement ends with a semicolon. In Scala—like in JavaScript and other scripting languages—a semicolon is never required if it falls just before the end of the line. A semicolon is also optional before an }
, an else
, and similar locations where it is clear from context that the end of a statement has been reached.
However, if you want to have more than one statement on a single line, you need to separate them with semicolons. For example,
if n > 0 then { r = r * n; n -= 1 }
A semicolon is needed to separate r = r * n
and n -= 1
. Because of the }
, no semicolon is needed after the second statement.
If you want to continue a long statement over two lines, make sure that the first line ends in a symbol that cannot be the end of a statement. An operator is often a good choice:
s = s + v * t + //
The + tells the parser that this is not the end0.5 * a * t * t
In practice, long expressions usually involve function or method calls, and then you don’t need to worry much—after an opening (
, the compiler won’t infer the end of a statement until it has seen the matching )
.
Many programmers coming from Java or C++ are initially uncomfortable about omitting semicolons. If you prefer to put them in, feel free to—they do no harm.
In Java, JavaScript, or C++, a block statement is a sequence of statements enclosed in { }
. You use a block statement whenever you need to put multiple actions in the body of a branch or loop statement.
In Scala, a { }
block contains a sequence of expressions, and the result is also an expression. The value of the block is the value of the last expression.
This feature can be useful if the initialization of a val
takes more than one step. For example,
var distance =
{ val dx = x - x0; val dy = y - y0; scala.math.sqrt(dx * dx + dy * dy) }
The value of the { }
block is the last expression, shown here in bold. The variables dx
and dy
, which were only needed as intermediate values in the computation, are neatly hidden from the rest of the program.
If you write the block on multiple lines, you can use indentation instead of braces:
distance =
val dx = x - x0
val dy = y - y0
scala.math.sqrt(dx * dx + dy * dy)
Block indentation is common with branches and loops:
if n % 2 == 0 then
a = a * a
n = n / 2
else
r = r * a
n -= 1
You can use braces, but in Scala 3, the “quiet” indentation style is preferred. Python programmers will rejoice.
In Scala, assignments have no value—or, strictly speaking, they have a value of type Unit
. Recall that the Unit
type is the equivalent of the void
type in Java and C++, with a single value written as ()
.
A block that ends with an assignment, such as
{ r = r * a; n -= 1 }
has a Unit
value. This is not a problem, just something to be aware of when defining functions—see Section 2.7. “Functions,” on page 27.
Since assignments have Unit
value, don’t chain them together.
i = j = 1 //
Does not set i to 1
The value of j = 1
is ()
, and it’s highly unlikely that you wanted to assign a Unit
to x
. (In fact, it is not easy to do—the variable i
would need to have type Unit
or Any
.) In contrast, in Java, C++, and Python, the value of an assignment is the value that is being assigned. In those languages, chained assignments are useful. In Scala, make two assignments:
j = 1
i = j
To print a value, use the print
or println
function. The latter adds a newline character after the printout. For example,
print("Answer: ")
println(42)
yields the same output as
println("Answer: " + 42)
Use string interpolation for formatted output:
println(f"Hello, $name! In six months, you’ll be ${age + 0.5}%7.2f years old.")
A formatted string is prefixed with the letter f
. It contains expressions that are prefixed with $
and optionally followed by C-style format strings. The expression $name
is replaced with the value of the variable name
. The expression ${age + 0.5}%7.2f
is replaced with the value of age + 0.5
, formatted as a floating-point number of width 7 and precision 2. You need {...}
around expressions that are not variable names.
Using the f
interpolator is better than using the printf
method because it is typesafe. If you accidentally use %f
with an expression that isn’t a number, the compiler reports an error.
Note
Formatted strings are one of three predefined string interpolators in the Scala library. With a prefix of s
, strings can contain delimited expressions with a $
prefix, but not format directives. With a prefix of raw
, escape sequences in a string are not evaluated. For example, raw"
is a newline"
starts with a backslash and the letter n, not a newline character.
To include $
and %
characters in a formatted string, double them. For example, f"$$$price: a 50%% discount"
yields a dollar sign followed by the value of price
and “a 50% discount”.
You can also define your own interpolators—see Exercise 12 on page 37. However, interpolators that produce compile-time errors (such as the f
interpolator) need to be implemented as “macros,” an advanced technique that is briefly introduced in Chapter 20.
You can read a line of input from the console with the readLine
method of the scala.io.StdIn
class. To read a numeric, Boolean, or character value, use readInt
, readDouble
, readByte
, readShort
, readLong
, readFloat
, readBoolean
, or readChar
. The readLine
method, but not the other ones, takes a prompt string:
import scala.io.*
val name = StdIn.readLine("Your name: ")
print("Your age: ")
val age = StdIn.readInt()
println(s"Hello, ${name}! Next year, you will be ${age + 1}.")
Caution
Some worksheet implementations do not handle console input. To make use of console input, compile and run a program, as shown in Section 2.10, “The Main Function,” on page 31.
Scala has the same while
loop as Java, JavaScript, C++, and Python. For example,
while n > 0 do
r = r * n
n -= 1
This is the “quiet” braceless syntax that is favored in Scala 3. However, you can use braces if you prefer:
while (n > 0) {
r = r * n
n -= 1
}
Tip
If the body of a while
loop gets long, and you use the braceless syntax, you can add end while
at the end to more clearly show the end of the loop:
while n > 0 do
r = r * n
//
Many more linesn -= 1
end while
Scala 3 does not have a do
/while
loop. In Java, JavaScript, or C++, one might use a loop such as the following for approximating a square root:
estimate = 1; //
Initial estimatedo { //
This is Javaprevious = estimate; //
Save previous estimateestimate = (estimate + a / estimate) / 2; //
Better estimate} while (scala.math.abs(estimate - previous) > EPSILON)
//
Keep going while consecutive estimates are too far apart
The do
/while
loop is used because one must enter the loop at least once.
In Scala, you can instead use a while
loop whose condition is a block:
while
val previous = estimate //
Work done in the blockestimate = (estimate + a / estimate) / 2 //
More work donescala.math.abs(estimate - previous) > EPSILON
//
This is the value of the block and the loop conditiondo () //
All work was done in the condition block
This may not be very pretty, but do
/while
loops are not common.
Scala has no direct analog of the for (initialize; test; update)
loop either. If you need such a loop, you have two choices. You can use a while
loop. Or, you can use a for
statement like this:
for i <- 1 to n do
r = r * i
You saw the to
method of the RichInt
class in Chapter 1. The call 1 to n
returns a Range
of the numbers from 1 to n
(inclusive).
The construct
for i <-
exprdo
makes the variable i
traverse all values of the expression to the right of the <-
. Exactly how that traversal works depends on the type of the expression. For a Scala collection, such as a Range
, the loop makes i
assume each value in turn.
Note
There is no val
or var
before the variable in the for
loop. The type of the variable is the element type of the collection. The scope of the loop variable extends until the end of the loop.
When traversing a string, you can loop over the index values:
val s = "Hello"
var sum = 0
for i <- 0 to s.length - 1 do
sum += s(i)
In this example, there is actually no need to use indexes. You can directly loop over the characters:
sum = 0
for ch <- "Hello" do sum += ch
In Scala, loops are not used as often as in other languages. As you will see in Chapter 12, you can often process the values in a sequence by applying a function to all of them, which can be done with a single method call.
Note
Scala has no break
or continue
statements to break out of a loop. What to do if you need a break
?
You can always replace break
statements with additional Boolean control variables. Alternatively, you can use the break
method in the Breaks
object:
import scala.util.control.Breaks.*
breakable {
for c <- "Hello, World!" do
if c == ',' then break //
Exits the breakable blockelse println(c)
}
Here, the control transfer is done by throwing and catching an exception, so you should avoid this mechanism when time is of essence.
Note
In Java, you cannot have two local variables with the same name and overlapping scope. In Scala, there is no such prohibition, and the normal shadowing rule applies. For example, the following is perfectly legal:
val k = 42
for k <- 1 to 10 do
println(k) //
Here k refers to the loop variable
for
LoopIn the preceding section, you saw the basic form of the for
loop. However, this construct is much richer in Scala than in Java, JavaScript, or C++. This section covers the advanced features.
You can have multiple generators of the form variable <-
expression. For example,
for
i <- 1 to 3
j <- 1 to 3
do
print(f"${10 * i + j}%3d")
//
Prints 11 12 13 21 22 23 31 32 33
A guard is a Boolean condition preceded by if
:
for
i <- 1 to 3
j <- 1 to 3
if i != j
do
print(f"${10 * i + j}%3d")
//
Prints 12 13 21 23 31 32
You can have any number of definitions, introducing variables that can be used inside the loop:
for
i <- 1 to 3
from = 4 - i
j <- from to 3
do
print(f"${10 * i + j}%3d")
//
Prints 13 22 23 31 32 33
Note
If you prefer, you can use semicolons instead of newlines to separate generators and definitions of a for
loop. Semicolons before an if
guard are optional.
for i <- 1 to 3; from = 4 - i; j <- from to 3 if i != j
do println(i * 10 + j)
The classic syntax uses parentheses instead of the do
keyword:
for (i <- 1 to 3; from = 4 - i; j <- from to 3 if i != j)
println(i * 10 + j)
Braces are OK too:
for { i <- 1 to 3; from = 4 - i; j <- from to 3 if i != j }
println(i * 10 + j)
When the body of the for
loop starts with yield
, the loop constructs a collection of values, one for each iteration:
val result = for i <- 1 to 10 yield i % 3
//
Yields Vector(1, 2, 0, 1, 2, 0, 1, 2, 0, 1)
This type of loop is called a for
comprehension.
The generated collection is compatible with the generator.
for c <- "Hello" yield (c + 1).toChar
//
Yields the string "Ifmmp"
Scala has functions in addition to methods. A method operates on an object, but a function doesn’t. C++ has functions as well, but in Java, you have to imitate them with static methods.
To define a function, specify the function’s name, parameters, and body. Then declare it outside a class or inside a block, like this:
def abs(x: Double) = if x >= 0 then x else -x
You must specify the types of all parameters. However, as long as the function is not recursive, you need not specify the return type. The Scala compiler determines the return type from the type of the expression to the right of the =
symbol.
Caution
There is some disagreement about the terminology of methods and functions in Scala. I follow the classic terminology where, unlike a function, a method has a special “receiver” or this
parameter.
For example, pow
in the scala.math
package is a function, but substring
is a method of the String
class.
When calling the pow
function, you supply all arguments in parentheses: pow(2, 4)
. When calling the substring
method, you supply a String
argument with the dot notation, and additional arguments in parentheses: "Hello".substring(2, 4)
. The "Hello"
argument is the “receiver” of the method invocation.
You declare methods with def
inside a class, trait, or object. But you can also use def
to declare “top-level” functions outside a class, and “nested” functions inside a block. They too will be compiled into methods in the Java virtual machine. Perhaps for that reason, some people call anything declared with def
a method. But in this book, a method only refers to a member of a class, trait, or object.
Everyone agrees that the “lambda expressions” that you will see in Chapter 12 are functions.
If the body of a function requires more than one expression, use a block. The last expression of the block becomes the value that the function returns. For example, the following function returns the value of r
after the for
loop.
def fac(n: Int) =
var r = 1
for i <- 1 to n do r = r * i
r
You can optionally add an end
statement to denote the end of a function definition:
def fac(n: Int) =
var r = 1
for i <- 1 to n do r = r * i
r
end fac
This makes sense when the function body spans many lines. You can also use braces:
def fac(n: Int) = {
var r = 1
for i <- 1 to n do r = r * i
r
}
Note that there is no return
keyword. In Scala, you cannot return a value from the middle of a function. Instead, organize your code so that the last expression of the function body yields the value to be returned.
Note
If you really need to return a value in deeply nested code without reaching the end of a function, you can use the NonLocalReturns
mechanism. See Chapter 12 for details.
With a recursive function, you must specify the return type. For example,
def fac(n: Int): Int = if n <= 0 then 1 else n * fac(n - 1)
Without the return type, the Scala compiler couldn’t verify that the type of n * fac(n - 1)
is an Int
.
Note
Some programming languages (such as ML and Haskell) can infer the type of a recursive function, using the Hindley-Milner algorithm. However, this doesn’t work well in an object-oriented language. Extending the Hindley-Milner algorithm so it can handle subtypes is still a research problem.
A function need not return any value. Consider this example:
def log(sb: StringBuilder, message: String) =
sb.append(java.time.Instant.now())
sb.append(": ")
sb.append(message)
sb.append("
")
The function appends a message with a time stamp.
Technically, the function returns a value, namely the value returned by the last call to append
. We aren’t interested in that value, whatever it may be. To clarify that the function is only called for its side effect, declare the return type as Unit
:
def log(sb: StringBuilder, message: String) : Unit = ...
A function can provide default arguments that are used when no explicit values are provided in a call. For example,
def decorate(str: String, left: String = "[", right: String = "]") =
left + str + right
This function has two parameters, left
and right
, with default arguments "["
and "]"
.
If you call decorate("Hello")
, you get "[Hello]"
. If you don’t like the defaults, supply your own: decorate("Hello", "<<<", ">>>")
.
If you supply fewer arguments than there are parameters, the defaults are applied from the end. For example, decorate("Hello", ">>>[")
uses the default value of the right
parameter, yielding ">>>[Hello]"
.
You can also specify the parameter names when you supply the arguments. For example,
decorate(left = "<<<", str = "Hello", right = ">>>")
The result is "<<<Hello>>>"
. Note that the named arguments need not be in the same order as the parameters.
Named arguments can make a function call more readable. They are also useful if a function has many parameters with default arguments.
You can mix unnamed and named arguments, provided the unnamed ones come first:
decorate("Hello", right = "]<<<") //
Calls decorate("Hello", "[", "]<<<")
Sometimes, it is convenient to implement a function that can take a variable number of arguments. The following example shows the syntax:
def sum(args: Int*) =
var result = 0
for arg <- args do result += arg
result
You can call this function with as many arguments as you like.
sum(1, 4, 9, 16, 25)
The function receives a single arguments of type Seq
, which we will discuss in Chapter 13. For now, all you need to know is that you can use a for
loop to visit each element.
If you already have a sequence of values, you cannot pass it directly to such a function. For example, the following is not correct:
sum(1 to 5) //
Error
If the sum
function is called with one argument, that must be a single integer, not a range of integers. The remedy is to tell the compiler that you want the argument to be considered a sequence. Use a postfix *
, like this:
sum((1 to 5)*) //
Consider 1 to 5 as an argument sequence
This call syntax is needed in a recursive definition:
def recursiveSum(args: Int*) : Int =
if args.length == 0 then 0
else args.head + recursiveSum(args.tail*)
Here, the head
of a sequence is its initial element, and tail
is a sequence of all other elements. That’s again a Seq
object, and we have to use a postfix *
to convert it to an argument sequence.
Every program must start somewhere. When running a compiled executable, the entry point is the function defined with the @main
annotation:
@main def hello() =
println("Hello, World!")
It does not matter what the main function is called.
To process command-line arguments, provide a parameter of type String*
:
@main def hello2(args: String*) =
println(s"Hello, ${args(1)}!")
You can also specify types for the command-line arguments:
@main def hello3(repetition: Int, name: String) =
println("Hello " * repetition + name)
Then the first command-line argument must be an integer (or, more accurately, a string containing an integer).
Parsers for the types Boolean
, Byte
, Short
, Int
, Long
, Float
, and Double
are supplied. You can parse other types—see Exercise 13 on page 37.
Note
When compiling a program with a @main
-annotated function, the compiler produces a class file whose name is the name of that function, not the name of the source file. For example, if the hello
function is in a file Main.scala
, compiling that file yields hello.class
.
You can declare a function without any parameters:
def words = scala.io.Source.fromFile("/usr/share/dict/words").mkString
You call the function as
words
without parentheses.
In contrast, if you define the function with an empty parameter list
def words() = scala.io.Source.fromFile("/usr/share/dict/words").mkString
the function is invoked with parentheses:
words()
Why would you ever want to omit the parentheses? In Scala, the convention is to drop parentheses if the function is “idempotent”—that is, if it always returns the same value.
For example, if you assume that the contents of the file /usr/share/dict/words
does not change, then the function without parentheses is the right choice.
Why wouldn’t you just use a variable?
val words = scala.io.Source.fromFile("/usr/share/dict/words").mkString
The value is set whether or not you use it. With a function, the computation is deferred until you invoke it.
Note
Scala 3 is stricter about the use of parentheses than prior versions. If you define a function with parentheses, you must invoke it with parentheses. Conversely, a function that was defined without parentheses must be called without them. For compatibility, this rule does not apply to legacy functions. The scala.math.random
function is definitely not idempotent. You expect each invocation of scala.math.random()
to return a different value. Nevertheless, it can also be called without parentheses.
Note
Should you define the main function without parentheses? If your program doesn’t read command-line arguments and it always does the same thing, go right ahead:
@main def hello4 =
println("Hello, World!")
When a val
is declared as lazy
, its initialization is deferred until it is accessed for the first time. For example,
lazy val words = scala.io.Source.fromFile("/usr/share/dict/words").mkString
(We will discuss file operations in Chapter 9. For now, just take it for granted that this call reads all characters from a file into a string.)
If the program never accesses words
, the file is never opened. To verify this, try it out in the REPL, but misspell the file name. There will be no error when the initialization statement is executed. However, if you access words
, you will get an error message that the file is not found.
Lazy values are useful to delay costly initialization statements. They can also deal with other initialization issues, such as circular dependencies. Moreover, they are essential for developing lazy data structures—see Chapter 13.
You can think of lazy values as halfway between val
and def
. Compare
val words1 =
println("words1: Reading file")
scala.io.Source.fromFile("/usr/share/dict/words").mkString
//
Evaluated as soon as words1 is definedlazy val words2 =
println("words2: Reading file")
scala.io.Source.fromFile("/usr/share/dict/words").mkString
//
Evaluated the first time words2 is useddef words3 =
println("words3: Reading file")
scala.io.Source.fromFile("/usr/share/dict/words").mkString
//
Evaluated every time words3 is used
Note
Laziness is not cost-free. Every time a lazy value is accessed, a method is called that checks, in a threadsafe manner, whether the value has already been initialized.
Scala exceptions work the same way as in Java, JavaScript, C++, or Python. When you throw an exception, for example
throw IllegalArgumentException("x should not be negative")
the current computation is aborted, and the runtime system looks for an exception handler that can accept an IllegalArgumentException
. Control resumes with the innermost such handler. If no such handler exists, the program terminates.
As in Java, the objects that you throw need to belong to a subclass of java.lang.Throwable
. However, unlike Java, Scala has no “checked” exceptions—you never have to declare that a function or method might throw an exception.
Note
In Java, “checked” exceptions are checked at compile time. If your method might throw an IOException
, you must declare it. This forces programmers to think where those exceptions should be handled, which is a laudable goal. Unfortunately, it can also give rise to monstrous method signatures such as void doSomething() throws IOException, InterruptedException, ClassNotFoundException
. Many Java programmers detest this feature and end up defeating it by either catching exceptions too early or using excessively general exception classes. The Scala designers decided against checked exceptions, recognizing that thorough compile-time checking isn’t always a good thing.
A throw
expression has the special type Nothing
. That is useful in if
/else
expressions. If one branch has type Nothing
, the type of the if
/else
expression is the type of the other branch. For example, consider
if x >= 0 then scala.math.sqrt(x)
else throw IllegalArgumentException("x should not be negative")
The first branch has type Double
, the second has type Nothing
. Therefore, the if
/else
expression also has type Double
.
The syntax for catching exceptions is modeled after the pattern matching syntax (see Chapter 14).
val url = URL("http://horstmann.com/fred.gif")
try
process(url)
catch
case _: MalformedURLException => println(s"Bad URL: $url")
case ex: IOException => println(ex)
The more general exception types must come after the more specific ones.
Note that you can use _
for the variable name if you don’t need it.
The try
/finally
statement lets you dispose of a resource whether or not an exception has occurred. For example:
val in = URL("http://horstmann.com/cay-tiny.gif").openStream()
try
process(in)
finally
println("Closing input stream")
in.close()
The finally
clause is executed whether or not the process
function throws an exception. The input stream is always closed.
This code is a bit subtle, and it raises several issues.
What if the URL
constructor or the openStream
method throws an exception? Then the try
block is never entered, and neither is the finally
clause. That’s just as well—in
was never initialized, so it makes no sense to invoke close
on it.
Why isn’t val in = URL(...).openStream()
inside the try
block? Then the scope of in
would not extend to the finally
clause.
What if in.close()
throws an exception? Then that exception is thrown out of the statement, superseding any earlier one. (This is just like in Java, and it isn’t very nice. Ideally, the old exception would stay attached to the new one.)
Note that try
/catch
and try
/finally
have complementary goals. The try
/catch
statement handles exceptions, and the try
/finally
statement takes some action (usually cleanup) when an exception is not handled. You can combine them into a single try
/catch
/finally
statement:
try
...
catch
...
finally
...
This is the same as
try
try
...
catch
...
finally
...
In practice, that combination is not often used because exceptions are usually caught far from where they are thrown, whereas cleanup is needed close to the point where exceptions can occur.
Note
The Try
class is designed to work with computations that may fail with exceptions. We will look at it more closely in Chapter 16. Here is a simple example:
import scala.io.*
import scala.util.*
val result =
for
a <- Try { StdIn.readLine("a: ").toInt }
b <- Try { StdIn.readLine("b: ").toInt }
yield a / b
If an exception occurs in either of the calls to toInt
, or because of division by zero, then result
is a Failure
object, containing the exception that caused the computation to fail. Otherwise, result
is a Success
object holding the result of the computation.
Note
Scala does not have an analog to the Java try
-with-resources statement. In Java, you can write
//
Javatry (Reader in = openReader(inPath); Writer out = openWriter(outPath)) {
//
Read from in, write to outprocess(in, out)
} //
No matter what, in and out correctly closed
This statement calls the close
method on all variables declared inside try (...)
, handling all tricky cases. For example, if in
was successfully initialized but the newBufferedWriter
method threw an exception, in
is closed but out
is not.
In Scala, the Using
helper handles this situation:
import scala.util.*
Using.Manager { use =>
val in = use(openReader(inPath))
val out = use(openWriter(outPath))
//
Read from in, write to outprocess(in, out)
} //
The reader and writer are closed
1. What does println(println("Hello"))
print, and why?
2. What is the value of an empty block expression {}
? What is its type?
3. Come up with one situation where the assignment x = y = 1
is valid in Scala. (Hint: Pick a suitable type for x
.)
4. Write a Scala equivalent for this loop in Java/JavaScript/C++ syntax:
for (int i = 10; i >= 0; i--) System.out.println(i);
5. The signum of a number is 1 if the number is positive, −1 if it is negative, and 0 if it is zero. Write a function that computes this value.
6. Write a function countdown(n: Int)
that prints the numbers from n
to 0
without returning a value.
7. Write a for
loop for computing the product of the Unicode codes of all letters in a string. For example, the product of the characters in "Hello"
is 9415087488L
.
8. Solve the preceding exercise without writing a loop. (Hint: Look at the StringOps
Scaladoc.)
9. Write a function product(s: String)
that computes the product, as described in the preceding exercises.
10. Make the function of the preceding exercise a recursive function.
11. Write a function that computes xn, where n is an integer. Use the following recursive definition:
xn = y ‧ y if n is even and positive, where y = xn/2.
xn = x ‧ xn − 1 if n is odd and positive.
x0 = 1.
xn = 1/x−n if n is negative.
12. Define a string interpolator date that produces a java.time.LocalDate
instance from year, month, and day values given as integer expressions (such as date"$year-$month-$day"
), strings (such as date"2099-12-31"
), or a mixture (such as date"$year-12-31"
). You need to define an “extension method,” like this:
extension (sc: StringContext)
def date(args: Any*): LocalDate = ...
args(i).asInstanceOf[Int]
is the value of the i
th interpolated expression as an integer. You get the strings in between the expressions as sc.parts
. Split along dashes and convert the strings to integers by calling toInt
. Then call the LocalDate.of
method.
13. To parse a command-line argument into an arbitrary type, you need to provide a “given instance.” For example, to parse a LocalDate
:
import java.time.*
import scala.util.*
given CommandLineParser.FromString[LocalDate] with
def fromString(s: String) = LocalDate.parse(s)
Write a Scala program that receives two dates on the command line and prints the number of days between them. Your main function should have two parameters of type LocalDate
.