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.
• The void
type is Unit
.
• Avoid using return
in a function.
• Beware of missing =
in a function definition.
• Exceptions work just like in Java or C++, but you use a “pattern matching” syntax for catch
.
• Scala has no checked exceptions.
Scala has an if
/else
construct with the same syntax as in Java or C++. However, in Scala, an if
/else
has a value, namely the value of the expression that follows the if
or else
. For example,
if (x > 0) 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) 1 else -1
This has the same effect as
if (x > 0) s = 1 else s = -1
However, the first form is better because it can be used to initialize a val
. In the second form, s
needs to be a var
.
(As already mentioned, semicolons are mostly optional in Scala—see Section 2.2, “Statement Termination,” on page 19.)
Java and C++ have a ?:
operator for this purpose. The expression
x > 0 ? 1 : -1 // Java or C++
is equivalent to the Scala expression if (x > 0) 1 else -1
. However, you can’t put statements inside a ?:
expression. The Scala if
/else
combines the if
/else
and ?:
constructs that are separate in Java and C++.
In Scala, every expression has a type. For example, the expression if (x > 0) 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) "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
. Their common supertype is called Any
. (See Section 8.11, “The Scala Inheritance Hierarchy,” on page 100 for details.)
If the else
part is omitted, for example in
if (x > 0) 1
then it is possible that the if
statement 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
statement without an else
is equivalent to
if (x > 0) 1 else ()
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.”)
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, when you type
if (x > 0) 1
else if (x == 0) 0 else -1
the REPL executes if (x > 0) 1
and shows the answer. Then it gets confused about the else
keyword.
If you want to break the line before the else
, use braces:
if (x > 0) { 1
} else if (x == 0) 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.
Tip
If you want to paste a block of code into the REPL without worrying about its nearsightedness, use paste mode. Type
:paste
Then paste in the code block and type Ctrl+D. The REPL will then analyze the block 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) { 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 = s0 + (v - v0) * t + // The + tells the parser that this is not the end
0.5 * (a - a0) * 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 )
.
In the same spirit, Scala programmers favor the Kernighan & Ritchie brace style:
if (n > 0) {
r = r * n
n -= 1
}
The line ending with a {
sends a clear signal that there is more to come.
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 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,
val distance = { val dx = x - x0; val dy = y - y0; 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.
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 * n; 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 25.
Since assignments have Unit
value, don’t chain them together.
x = y = 1 // No
The value of y = 1
is ()
, and it’s highly unlikely that you wanted to assign a Unit
to x
. (In contrast, in Java and C++, the value of an assignment is the value that is being assigned. In those languages, chained assignments are useful.)
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)
There is also a printf
function with a C-style format string:
printf("Hello, %s! You are %d years old.%n", name, age)
Or better, use string interpolation
print(f"Hello, $name! In six months, you'll be ${age + 0.5}%7.2f years old.%n")
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 type-safe. If you accidentally use %f
with an expression that isn’t a number, the compiler reports an error.
Formatted strings are one of three predefined string interpolators in the Scala library. With a prefix of s
, strings can contain expressions 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 a $ sign in an interpolated string, double it. For example, s"$$$price"
yields a dollar sign followed by the value of price
.
You can also define your own interpolators—see Exercise 11 on page 32. However, interpolators that produce compile-time errors (such as the f
interpolator) need to be implemented as “macros,” an experimental Scala feature that is beyond the scope of this book.
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}.")
Scala has the same while
and do
loops as Java and C++. For example,
while (n > 0) {
r = r * n
n -= 1
}
Scala has no direct analog of the for (
initialize;
test;
update)
loop. 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)
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)
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)
sum += s(i)
In this example, there is actually no need to use indexes. You can directly loop over the characters:
var sum = 0
for (ch <- "Hello") 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
? Here are a few options:
• Use a Boolean control variable.
• Use nested functions—you can return
from the middle of a function.
• Use the break
method in the Breaks
object:
import scala.util.control.Breaks._
breakable {
for (...) {
if (...) break; // Exits the breakable block
...
}
}
Here, the control transfer is done by throwing and catching an exception, so you should avoid this mechanism when time is of essence.
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 n = ...
for (n <- 1 to 10) {
// Here n refers to the loop variable
}
In the preceding section, you saw the basic form of the for
loop. However, this construct is much richer in Scala than in Java or C++. This section covers the advanced features.
You can have multiple generators of the form variable <-
expression. Separate them by semicolons. For example,
for (i <- 1 to 3; j <- 1 to 3) print(f"${10 * i + j}%3d")
// Prints 11 12 13 21 22 23 31 32 33
Each generator can have a guard, a Boolean condition preceded by if
:
for (i <- 1 to 3; j <- 1 to 3 if i != j) print(f"${10 * i + j}%3d")
// Prints 12 13 21 23 31 32
Note that there is no semicolon before the if
.
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) print(f"${10 * i + j}%3d")
// Prints 13 22 23 31 32 33
When the body of the for
loop starts with yield
, the loop constructs a collection of values, one for each iteration:
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 first generator.
for (c <- "Hello"; i <- 0 to 1) yield (c + i).toChar
// Yields "HIeflmlmop"
for (i <- 0 to 1; c <- "Hello") yield (c + i).toChar
// Yields Vector('H', 'e', 'l', 'l', 'o', 'I', 'f', 'm', 'm', 'p')
If you prefer, you can enclose the generators, guards, and definitions of a for
loop in braces, and you can use newlines instead of semicolons to separate them:
for { i <- 1 to 3
from = 4 - i
j <- from to 3 }
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 like this:
def abs(x: Double) = if (x >= 0) 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.
If the body of the 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) r = r * i
r
}
There is no need for the return
keyword in this example. It is possible to use return
as in Java or C++, to exit a function immediately, but that is not commonly done in Scala.
Tip
While there is nothing wrong with using return
in a named function (except the waste of seven keystrokes), it is a good idea to get used to life without return
. Pretty soon, you will be using lots of anonymous functions, and there, return
doesn’t return a value to the caller but breaks out to the enclosing named function. Think of return
as a kind of break
statement for functions, and only use it when you want that breakout functionality.
With a recursive function, you must specify the return type. For example,
def fac(n: Int): Int = if (n <= 0) 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.
You can provide default arguments for functions that are used when you don’t specify explicit values. 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 default parameters.
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) result += arg
result
}
You can call this function with as many arguments as you like.
val s = sum(1, 4, 9, 16, 25)
The function receives a single parameter 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:
val s = 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 parameter to be considered an argument sequence. Append : _*
, like this:
val s = 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) 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
, and we have to use : _*
to convert it to an argument sequence.
Caution
When you call a Java method with variable arguments of type Object
, such as PrintStream.printf
or MessageFormat.format
, you need to convert any primitive types by hand. For example,
val str = MessageFormat.format("The answer to {0} is {1}",
"everything", 42.asInstanceOf[AnyRef])
This is the case for any Object
parameter, but I mention it here because it is most common with varargs methods.
Scala has a special notation for a function that returns no value. If the function body is enclosed in braces without a preceding =
symbol, then the return type is Unit
. Such a function is called a procedure. A procedure returns no value, and you only call it for its side effect. For example, the following procedure prints a string inside a box, like
-------
|Hello|
-------
Since the procedure doesn’t return any value, we omit the =
symbol.
def box(s : String) { // Look carefully: no =
val border = "-" * (s.length + 2)
print(f"$border%n|$s|%n$border%n")
}
Some people (not me) dislike this concise syntax for procedures and suggest that you always use an explicit return type of Unit
:
def box(s : String): Unit = {
...
}
Caution
The concise procedure syntax can be a surprise for Java and C++ programmers. It is a common error to accidentally omit the =
in a function definition. You then get an error message at the point where the function is called: You are told that Unit
is not acceptable at that location.
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 Section 13.12, “Streams,” on page 189.
You can think of lazy values as halfway between val
and def
. Compare
val words = scala.io.Source.fromFile("/usr/share/dict/words").mkString
// Evaluated as soon as words is defined
lazy val words = scala.io.Source.fromFile("/usr/share/dict/words").mkString
// Evaluated the first time words is used
def words = scala.io.Source.fromFile("/usr/share/dict/words").mkString
// Evaluated every time words 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 or C++. When you throw an exception, for example
throw new 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) { sqrt(x)
} else throw new 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 = new URL("http://horstmann.com/fred-tiny.gif")
try {
process(url)
} catch {
case _: MalformedURLException => println(s"Bad URL: $url")
case ex: IOException => ex.printStackTrace()
}
As in Java or C++, the more general exception types should 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 = new URL("http://horstmann.com/fred.gif").openStream()
try {
process(in)
} finally {
in.close()
}
The finally
clause is executed whether or not the process
function throws an exception. The reader
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 = new 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 { ... }
However, that combination is rarely useful.
Note
Scala does not have an analog to the Java try
-with-resources statement. Consider using the scala-ARM library (http://jsuereth.com/scala-arm
). Then you can write
import resource._
import java.nio.file._
for (in <- resource(Files.newBufferedReader(inPath));
out <- resource(Files.newBufferedWriter(outPath))) {
...
}
Note
The Try
class is designed to work with computations that may fail with exceptions. We will look at it more closely in Chapter 17. Here is a simple example:
import scala.io._
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.
1. 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.
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 the Java loop
for (int i = 10; i >= 0; i--) System.out.println(i);
5. Write a procedure countdown(n: Int)
that prints the numbers from n
to 0.
6. 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
.
7. Solve the preceding exercise without writing a loop. (Hint: Look at the StringOps
Scaladoc.)
8. Write a function product(s : String)
that computes the product, as described in the preceding exercises.
9. Make the function of the preceding exercise a recursive function.
10. 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.
Don’t use a return
statement.
11. Define a string interpolator date
so that you can define a java.time.LocalDate
as date"$year-$month-$day"
. You need to define an “implicit” class with a date
method, like this:
implicit class DateInterpolator(val sc: StringContext) extends AnyVal {
def date(args: Any*): LocalDate = . . .
}
args(i)
is the value of the i
th expression. Convert each to a string and then to an integer, and pass them to the LocalDate.of
method. If you already know some Scala, add error handling. Throw an exception if there aren’t three arguments, or if they aren’t integers, or if they aren’t separated by dashes. (You get the strings in between the expressions as sc.parts
.)