Chapter 2. Control Structures and Functions

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.

2.1 Conditional Expressions

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.

Images 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.

Images 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.

Images 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.

Images 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.

2.2 Statement Termination

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 end
  0.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.

2.3 Block Expressions and Assignments

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

2.4 Input and Output

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.

Images 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}.")

Images 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.

2.5 Loops

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
}

Images 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 lines
  n -= 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 estimate
do { // This is Java
  previous = estimate; // Save previous estimate
  estimate = (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 block
  estimate = (estimate + a / estimate) / 2 // More work done
  scala.math.abs(estimate - previous) > EPSILON
    // This is the value of the block and the loop condition
do () // 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 <- expr do

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.

Images 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.

Images 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 block
      else 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.

Images 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

2.6 More about the for Loop

In 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

Images 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"

2.7 Functions

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.

Images 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.

Images 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.

Images 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 = ...

2.8 Default and Named Arguments L1

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", "[", "]<<<")

2.9 Variable Arguments L1

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.

2.10 The Main Function

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.

Images 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.

2.11 Functions without Parameters

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.

Images 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.

Images 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!")

2.12 Lazy Values L1

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 defined
lazy val words2 =
  println("words2: Reading file")
  scala.io.Source.fromFile("/usr/share/dict/words").mkString
  // Evaluated the first time words2 is used
def words3 =
  println("words3: Reading file")
  scala.io.Source.fromFile("/usr/share/dict/words").mkString
  // Evaluated every time words3 is used

Images 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.

2.13 Exceptions

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.

Images 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.

Images 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.

Images Note

Scala does not have an analog to the Java try-with-resources statement. In Java, you can write

// Java
try (Reader in = openReader(inPath); Writer out = openWriter(outPath)) {
  // Read from in, write to out
  process(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 out
  process(in, out)
} // The reader and writer are closed

Exercises

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 = yy if n is even and positive, where y = xn/2.

  • xn = xxn − 1 if n is odd and positive.

  • x0 = 1.

  • xn = 1/xn 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 ith 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.114.245