Chapter 2. Control Structures and Functions

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.

2.1 Conditional Expressions

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.”)


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



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



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


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) { 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.

2.3 Block Expressions and Assignments

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

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)

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.


Image Note

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

2.5 Loops

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.


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


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



Image 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 n = ...
for (n <- 1 to 10) {
  // Here n refers to the loop variable
}


2.6 Advanced for Loops

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')


Image Note

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 }


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


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


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


2.8 Default and Named Arguments Image

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

2.9 Variable Arguments Image

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.


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


2.10 Procedures

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 = {
  ...
}


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


2.11 Lazy Values Image

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


Image 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.12 Exceptions

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.


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


Image 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))) {
  ...
}



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


Exercises

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 / xn 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 ith 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.)

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

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