Chapter 2. Type Less, Do More

This chapter continues our tour of Scala features that promote succinct, flexible code. We’ll discuss organization of files and packages, importing other types, variable and method declarations, a few particularly useful types, and miscellaneous syntax conventions.

New Scala 3 Syntax

If you have prior Scala experience, Scala 3 introduces a new optional braces syntax that makes it look a lot more like Python or Haskell, where Java-style curly braces ({…}) are replaced with indentation that becomes significant. The examples in the previous chapter and throughout the book use it.

This syntax has several benefits. It is more concise. For Python developers who decide to learn Scala, it will feel more familiar to them (and vice versa).

There is also a new syntax for control structures like for loops and if expressions. For example, if condition then … instead of the older if (condition) …. Also, for … do println(…) instead of for {…} println(…).

The disadvantage of these changes is that they are strictly not necessary. Some breaking changes in Scala 3 are necessary to move the language forward, but you could argue these syntax changes aren’t required. You also have to be careful to use tabs and spaces consistently for indentation.

The new constructs are the defaults supported by the compiler, but using the compiler flags -old-syntax and -noindent will enforce the old syntax constructs. Another flag, -new-syntax makes the new keywords then and do required. Finally, the compiler can now rewrite your code to use whichever style you prefer. Add the -rewrite compiler flag, for example -rewrite -new-syntax.

I opposed these changes initially, because they aren’t strictly necessary. However, now that I have worked with them, I believe the advantages outweigh the disadvantages. I also suspect that the syntax changes are the future for Scala and eventually the old syntax could be deprecated. We’ll see. Hence, I have chosen to use the new conventions throughout this edition of the book. I will mention other pros and cons of these changes as we explore examples.

OldVsNewSyntax provides examples of the old and new syntax.

Warning

Even if you continue using the brace syntax, the compiler now requires indentation to use tabs and spaces consistently, unless you use the -noindent compiler flag.

Semicolons

Semicolons are expression delimiters and they are inferred. Scala treats the end of a line as the end of an expression, except when it can infer that the expression continues to the next line, even for the following example:

scala> val s = "hello"
     |  + "world"
     |  + "!"
val s: String = helloworld!

The Scala 2 REPL was more aggressive at interpreting the end of a line as the end of an expression, so the previous example would infer “hello” for the definition of s and then throw an error for the next two lines.

Tip

When using the Scala 2 REPL, use the :paste mode when multiple lines need to be parsed as a whole. Enter :paste, followed by the code you want to enter, then finish with Ctrl-D.

Conversely, you can put multiple expressions on the same line, separated by semicolons.

Variable Declarations

Scala allows you to decide whether a variable is immutable (read-only) or not (read-write) when you declare it. We’ve already seen that an immutable “variable” is declared with the keyword val:

val array: Array[String] = new Array(5)

Scala is like Java in that most variables are actually references to heap-allocated objects. Hence, the array reference cannot be changed to point to a different Array, but the array elements themselves are mutable, so the elements can be modified:

scala> val array: Array[String] = new Array(5)
val array: Array[String] = Array(null, null, null, null, null)

scala> array = new Array(2)
1 |array = new Array(2)
  |^^^^^^^^^^^^^^^^^^^^
  |Reassignment to val array

scala> array(0) = "Hello"

scala> array
val res1: Array[String] = Array(Hello, null, null, null, null)

A val must be initialized when it is declared, except in certain contexts like abstract fields in type declarations.

Similarly, a mutable variable is declared with the keyword var and it must also be initialized immediately (in most cases), even though it can be changed later:

scala> var stockPrice: Double = 100.0
var stockPrice: Double = 100.0

scala> stockPrice = 200.0
stockPrice: Double = 200.0

To be clear, we changed the value of stockPrice itself. However, the “object” that stockPrice refers to can’t be changed, because Doubles in Scala are immutable.

In Java, so-called primitive types, char, byte, short, int, long, float, double, and boolean, are fundamentally different than reference objects. Indeed, there is no object and no reference, just the “raw” value. Scala tries to be consistently object-oriented, so these types are actually objects with methods, like reference types (see ReferenceVsValueTypes). However, Scala compiles to primitives where possible, giving you the performance benefit they provide (see SpecializationForValueTypes for details).

Consider the following REPL session, where we define a Person class with immutable first and last names, but a mutable age (because people age, I guess). The parameters are declared with val and var, respectively, making them both fields in Person:

// src/script/scala/progscala3/typelessdomore/Human.scala
scala> class Human(val name: String, var age: Int)
// defined class Human

scala> val p = new Human("Dean Wampler", 29)
val p: Human = Human@165a128d

scala> p.name
val res0: String = Dean Wampler

scala> p.name = "Buck Trends"
1 |p.name = "Buck Trends"
  |^^^^^^^^^^^^^^
  |Reassignment to val name

scala> p.name
val res1: String = Dean Wampler

scala> p.age
val res2: Int = 29

scala> p.age = 30

scala> p.age
val res3: Int = 30

scala> p.age = 31; p.age  // Use semicolon to join two expressions...
val res4: Int = 31
Note

The var and val keywords only specify whether the reference can be changed to refer to a different object (var) or not (val). They don’t specify whether or not the object they reference is mutable.

Use immutable values whenever possible to eliminate a class of bugs caused by mutability. For example, a mutable object is dangerous as a key in hash-based maps. If the object is mutated, the output of the hashCode method will change, so the corresponding value won’t be found at the original location.

More common is unexpected behavior when an object you are using is being changed by another thread. Borrowing a phrase from Quantum Physics, these bugs are spooky action at a distance. Nothing you are doing locally accounts for the unexpected behavior; it’s coming from somewhere else.

These are the most pernicious bugs in multithreaded programs, where synchronized access to shared, mutable state is required, but difficult to get right. Using immutable values eliminates these issues.

Ranges

Sometimes we need a sequence of numbers from some start to finish. A Range literal is just what we need. The following examples show how to create ranges for the types that support them, Int, Long, Char, BigInt, which represents integers of arbitrary size, and BigDecimal, which represents floating-point numbers of arbitrary size. Float and Double were supported in Scala 2, but floating point arithmetic makes range calculations error prone. Hence, Scala 3 drops ranges for Float and Double.

You can create ranges with an inclusive or exclusive upper bound, and you can specify an interval not equal to one (some output elided to fit):

scala> 1 to 10                // Int range inclusive, interval of 1, (1 to 10)
val res0: scala.collection.immutable.Range.Inclusive = Range 0 to 10

scala> 1 until 10             // Int range exclusive, interval of 1, (1 to 9)
val res1: Range = Range 0 until 10

scala> 1 to 10 by 3           // Int range inclusive, every third.
val res2: Range = inexact Range 0 to 10 by 3

scala> 10 to 1 by -3          // Int range inclusive, every third, counting down.
val res3: Range = Range 10 to 1 by -3

scala> 1L to 10L by 3         // Long
val res4: ...immutable.NumericRange[Long] = NumericRange 1 to 10 by 3

scala> 'a' to 'g' by 3         // Char
val res5: ...immutable.NumericRange[Char] = NumericRange a to g by

scala> BigInt(1) to BigInt(10) by 3
val res6: ...immutable.NumericRange[BigInt] = NumericRange 1 to 10 by 3

scala> BigDecimal(1.1) to BigDecimal(10.3) by 3.1
val res7: ...immutable.NumericRange.Inclusive[BigDecimal] =
  NumericRange 1.1 to 10.3 by 3.1

Partial Functions

A PartialFunction[A,B] is a special kind of function with its own literal syntax. A is the type of the single parameter the function accepts and B is the return type.

The literal syntax for a PartialFunction consists only of case clauses, which we saw in ASampleApplication, that do pattern matching on the input to the function. There is no function parameter shown explicitly, but when each input is processed, it is passed to the body of the partial function.

For comparison, is a regular function that does pattern matching and similar partial function, adapted from the example we explored in ASampleApplication:

// src/script/scala/progscala3/typelessdomore/FunctionVsPartialFunction.scala
scala> import progscala3.introscala.shapes._

scala> val func: Message => String = message => message match
     |   case Exit => "Got Exit"
     |   case Draw(shape) => s"Got Draw($shape)"
     |   case Response(str) => s"Got Response($str)"

scala> val pfunc: PartialFunction[Message, String] =
     |   case Exit => "Got Exit"
     |   case Draw(shape) => s"Got Draw($shape)"
     |   case Response(str) => s"Got Response($str)"

scala> func(Draw(Circle(Point(0.0,0.0), 1.0)))
     | pfunc(Draw(Circle(Point(0.0,0.0), 1.0)))
     | func(Response(s"Say hello to pi: 3.14159"))
     | pfunc(Response(s"Say hello to pi: 3.14159"))
val res0: String = Got Draw(Circle(Point(0.0,0.0),1.0))
val res1: String = Got Draw(Circle(Point(0.0,0.0),1.0))
val res2: String = Got Response(Say hello to pi: 3.14159)
val res3: String = Got Response(Say hello to pi: 3.14159)

Function definitions can be a little harder to read than method definition. The function func is a named function of type Message => String. The equal sign starts the body, message => message match ....

The partial function, pfunc, is simpler. It’s type is PartialFunction[Message, String]. There is no argument list, just a set of case match clauses, which happen to be identical to the clauses in func.

The concept of a partial function is simpler than it might appear. In essence, a partial function will only handle certain inputs, so don’t send it something it doesn’t know how to handle. A classic example from mathematics is division, x/y, which is undefined when the denominator y is 0. Hence, division is a partial function.

If a partial function is called with an input that doesn’t match one of the case clauses, a MatchError is thrown at runtime. Both func and pfunc are actually total, because they handle all possible Message arguments. Try commenting out the case Exit clauses in both func and pfunc. You’ll get a a compiler warning for func, because it can determine that the match clauses don’t handle all possible inputs. It won’t complain about pfunc, because that situation is by design.

You can test if a PartialFunction will match an input using the isDefinedAt method. This function avoids the risk of throwing a MatchError exception.

You can also “chain” PartialFunctions together: pf1.orElse(pf2).orElse(pf3) …. If pf1 doesn’t match, then pf2 is tried, then pf3, etc. A MatchError is only thrown if none of them matches.

Let’s explore these points with the following example:

// src/script/scala/progscala3/typelessdomore/PartialFunctions.scala

val pfs: PartialFunction[Any,String] =                               1
  case s:String => "YES"
val pfd: PartialFunction[Any,String] = {                             2
  case d:Double => "YES"
}

val pfsd = pfs.orElse(pfd)                                           3
1

A partial function that only matches on strings, using the braceless syntax.

2

A partial function that only matches on doubles, using braces.

3

Combine the two functions to construct a new partial function that matches on strings and doubles.

The next block of code in the script tries different values with the three partial functions to confirm expected behavior. Note that integers are not handled by any combination. A helper function tryPF is used to try the partial function and catch possible MatchError exceptions. So, a string is returned for both success and failure:

def tryPF(x: Any, f: PartialFunction[Any,String]): String =
  try f(x)
  catch case _: MatchError => "ERROR!"

assert(tryPF("str", pfs)  == "YES")
assert(tryPF("str", pfd)  == "ERROR!")
assert(tryPF("str", pfsd) == "YES")
assert(tryPF(3.142, pfs)  == "ERROR!")
assert(tryPF(3.142, pfd)  == "YES")
assert(tryPF(3.142, pfsd) == "YES")
assert(tryPF(2, pfs)      == "ERROR!")
assert(tryPF(2, pfd)      == "ERROR!")
assert(tryPF(2, pfsd)     == "ERROR!")

assert(pfs.isDefinedAt("str")  == true)
assert(pfd.isDefinedAt("str")  == false)
assert(pfsd.isDefinedAt("str") == true)
assert(pfs.isDefinedAt(3.142)  == false)
assert(pfd.isDefinedAt(3.142)  == true)
assert(pfsd.isDefinedAt(3.142) == true)
assert(pfs.isDefinedAt(2)      == false)
assert(pfd.isDefinedAt(2)      == false)
assert(pfsd.isDefinedAt(2)     == false)

Finally, we can lift a partial function into a regular (“total”) function that returns an option, Some(value), when the partial function is defined for the input argument and None when it isn’t. We can also unlift a single-parameter function. Here is a session that uses pfs:

scala> val fs = pfs.lift
val fs: Any => Option[String] = <function1>

scala> fs("str")
val res0: Option[String] = Some(YES)

scala> fs(3.142)
val res1: Option[String] = None

scala> val pfs2 = fs.unlift
val pfs2: PartialFunction[Any, String] = <function1>

scala> pfs2("str")
val res3: String = YES

scala> tryPF(3.142, pfs2)  // Use tryPF we defined above
val res4: String = ERROR!

Infix Operator Notation

In the previous example, we combined two partial functions using orElse. This can be written equivalently in two ways:

val pfsd1 = pfs.orElse(pfd)
val pfsd2 = pfs orElse pfd

When a method takes a single parameter, you can drop the period after the object and drop the parentheses around the supplied argument. In this case, pfs orElse pfd has a cleaner appearance than pfs.orElse(pfd), which is why this syntax is popular. This notation is called infix notation, because orElse is between the object and argument. This syntax is also called operator notation, because it is especially popular when writing libraries where algebraic notation is convenient. For example, you can write your own libraries for matrices and define a method named * for matrix multiplication using the * “operator”. Then you can write expressions like val matrix3 = matrix1 * matrix2.

Tip

Scala method names can use most non-alphanumeric characters. When methods are called that take a single parameter, infix operator notation can be used where the period after the object and the parentheses around the supplied argument can be dropped.

Method Declarations

Let’s explore method definitions, using a modified version of our Shapes hierarchy from before.

Method Default and Named Parameters

Here is an updated Point case class:

// src/main/scala/progscala3/typelessdomore/shapes/Shapes.scala
package progscala3.typelessdomore.shapes

case class Point(x: Double = 0.0, y: Double = 0.0):                  1
  def shift(deltax: Double = 0.0, deltay: Double = 0.0) =            2
    copy(x + deltax, y + deltay)
1

Define Point with default initialization values (as before). For case classes, both x and y are automatically immutable (val) fields.

2

A new shift method for creating a new Point instance, offset from the existing Point. It uses the copy method that is also created automatically for case classes.

The copy method allows you to construct new instances of a case class while specifying just the fields that are changing. This is very useful for larger case classes:

scala> val p1 = new Point(x = 3.3, y = 4.4)    // Used named arguments explicitly.
val p1: Point = Point(3.3,4.4)

scala> val p2 = p1.copy(y = 6.6)  // Copied with a new y value.
val p2: Point = Point(3.3,6.6)

Named arguments make client code more readable. They also help avoid bugs when a parameter list has several fields of the same type or it has a lot of parameters. It’s easy to pass values in the wrong order. Of course, it’s better to avoid such parameter lists in the first place.

Methods with Multiple Parameter Lists

Next, consider the following changes to Shape.draw():

abstract class Shape():
  def draw(offset: Point = Point(0.0, 0.0))(f: String => Unit): Unit =
    f(s"draw($offset, ${this})")

Circle, Rectangle, and +Triangle are unchanged and not shown.

Now draw has two parameter lists, each of which has a single parameter, rather than a single parameter list with two parameters. The first parameter list lets you specify an offset point where the shape will be drawn. It has a default value of Point(0.0, 0.0), meaning no offset. The second parameter list is the same as in the original version of draw, a function that does the drawing.

You can have as many parameter lists as you want, but it’s rare to use more than two.

So, why allow more than one parameter list? Multiple lists promote a very nice block-structure syntax when the last parameter list takes a single function. Here’s how we might invoke this new draw method to draw a Circle at an offset:

val s = Circle(Point(0.0, 0.0), 1.0)
s.draw(Point(1.0, 2.0))(str => println(str))

Scala lets us replace parentheses with curly braces around a supplied argument (like a function literal) for a parameter list that has a single parameter. So, this line can also be written this way:

s.draw(Point(1.0, 2.0)){str => println(str)}

Suppose the function literal is too long for one line or it has multiple statements or expressions? We can rewrite it this way:

s.draw(Point(1.0, 2.0)) { str =>
  println(str)
}

Or equivalently:

s.draw(Point(1.0, 2.0)) {
  str => println(str)
}

If you use the traditional curly-brace syntax for Scala, it looks like a typical block of code we use with constructs like if and for expressions, method bodies, etc. However, the {…} block is still a function literal we are passing to draw.

So, this “syntactic sugar” of using {…} instead of (…) looks better with longer function literals; they look more like the block structure syntax we know.

Unfortunately, the new optional braces syntax doesn’t work here:

scala> s.draw(Point(1.0, 2.0)):
     |   str => println(str)
2 |  str => println(str)
  |      ^
  |      parentheses are required around the parameter of a lambda
  |      This construct can be rewritten automatically under -rewrite.
1 |s.draw(Point(1.0, 2.0)):
  |^
  |not a legal formal parameter
2 |  str => println(str)

However, there is an experimental compiler flag -Yindent-colons that enables this capability, but it remains experimental (at the time of this writing), because it “is more contentious and less stable than the rest of the significant indentation scheme.” (Quote from this Dotty documentation.)

Back to using parentheses or braces, if we use the default value for offset, the first set of parentheses is still required. Otherwise, the function would be parsed as the offset, triggering an error.

s.draw() {
  str => println(str)
}

To be clear, draw could just have a single parameter list with two values, like Java methods. If so, the client code would look like this:

s.draw(Point(1.0, 2.0), str => println(str))

That’s not nearly as clear and elegant. It would also prevent us from using the default value for the offset.

By the way, we can can simplify our expressions even more. str => println(str) is an anonymous function that takes a single string argument and passes it to println. Although, println is implemented as a method in the Scala library, it can also be used as a function that takes a single string argument! Hence, the following two lines behave the same:

s.draw(Point(1.0, 2.0))(str => println(str))
s.draw(Point(1.0, 2.0))(println)

To be clear, the are not identical, they just do the same thing. In the first example, we pass an anonymous function that calls println. In the second example, we use println as a named function directly. Scala handles converting methods to functions in situations like this.

Another advantage of allowing two or more parameter lists is that we can use one or more lists for normal parameters and other lists for using clauses, formerly known as implicit parameter lists. These are parameter lists declared with the using or implicit keyword. When the methods are called, we can either explicitly specify arguments for these parameters, or we can let the compiler fill them in using a suitable value that’s in scope. Using clauses provides a more flexible alternative to parameters with default values. Let’s explore an example from the Scala library that uses this mechanism, Futures.

A Taste of Futures

The scala.concurrent.Future API is another tool for concurrency. Akka uses Futures, but you can use them separately when you don’t need the full capabilities of actors.

When you wrap some work in a Future, the work is executed asynchronously and the Future API provides various ways to process the results, such as providing callbacks that will be invoked when the result is ready. Let’s use callbacks here and defer discussion of the rest of the API until ToolsForConcurrency.

The following example fires off five work items concurrently and handles the results as they finish:

// src/script/scala/progscala3/typelessdomore/Futures.scala
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global            1
import scala.util.{Failure, Success}

def sleep(millis: Long) = Thread.sleep(millis)                       2

(1 to 5) foreach { i =>
  val future = Future {                                              3
    val duration = (math.random * 1000).toLong
    sleep(duration)
    if i == 3 then throw new RuntimeException(s"$i -> $duration")
    duration
  }
  future onComplete {                                                4
    case Success(result)    => println(s"Success! #$i -> $result")
    case Failure(throwable) => println(s"FAILURE! #$i -> $throwable")
  }
}
sleep(1000)  // Wait long enough for the "work" to finish.
println("Finished!")
1

We’ll discuss this import below.

2

A sleep method to simulate staying busy for a period amount of time.

3

Pass a block of work to the scala.concurrent.Future.apply method. It calls sleep with a duration, a randomly generated number of milliseconds between 0 and 1000, which it will also return. However, if i equals 3, we throw an exception.

4

Use onComplete to assign a partial function to handle the computation result. Notice that the expected output is either scala.util.Success wrapping a value or scala.util.Failure wrapping an exception.

Success and Failure are subclasses of scala.util.Try, which encapsulates try {…} catch {…} clauses with less boilerplate. We can handle successful code and possible exceptions more uniformly. See TryContainer for further discussion.

When we iterate through a Range of integers from 1 to 5, inclusive, we construct a Future with a block of work to do. Future.apply returns a new Future instance immediately. The body is executed asynchronously on another thread. The onComplete callback we register will be invoked when the body completes.

A final sleep call waits before exiting to allow the futures to finish.

A sample run might go like this, where the order of the results and the numbers on the right-hand side are arbitrary, as expected:

Success! #2 -> 178
Success! #1 -> 207
FAILURE! #3 -> java.lang.RuntimeException: 3 -> 617
Success! #5 -> 738
Success! #4 -> 938
Finished!

You might wonder about the “body of work” we’re passing to Future.apply. Is it a function or something else? Here is part of the declaration of Future.apply

apply[T](body: => T)(/* explained below */): Future[T]

Note how the type of body is declared, => T. This is called a by-name parameter. We are passing something that will return a T instance, but we want to evaluate body lazily. Go back to the example body we passed to Future.apply above. We did not want that code evaluated before it was passed to Future.apply. We wanted it evaluated inside the Future after construction. This is what by-name parameters do for us. We can pass a block of code that will be evaluated only when needed. The implementation of Future.apply evaluates this code.

Okay, let’s finally get back to implicit parameters. Note the second import statement:

import scala.concurrent.ExecutionContext.Implicits.global

Future methods use an ExecutionContext to run code in separate threads, providing concurrency. These methods use a given value of an ExecutionContext. For example, here’s the whole Future.apply declaration (using Scala 3 syntax):

apply[T](body: => T)(using executor: ExecutionContext): Future[T]

In the Scala 2 library, the implicit keyword is used instead of using. The second parameter list is called a using clause.

Because this parameter is in its own parameter list starting with using or implicit, users of Future.apply don’t have to pass a value explicitly. This reduces code boilerplate. We imported the default ExecutionContext.global value that is declared as given (or implicit in Scala 2). It uses a thread pool with a work-stealing algorithm to balance the load and optimize performance.

We can tailor how threads are used by passing our own ExecutionContext explicitly:

Future(work)(using someExecutionContext)

Alternatively, we can declare our own given value that will be used implicitly when Future.apply is called:

given myEC as MyCustomExecutionContext(arguments)
...
val future = Future(work)

The global value is declared in a similar way, but our given value will take precedence.

The Future.onComplete method we used also has a using clause:

abstract def onComplete[U](
  f: (Try[T]) => U)(using executor: ExecutionContext): Unit

So, when global is imported into the current scope, the compiler will use it when methods are called that have a using clause with an ExecutionContext parameter, unless we specify a value explicitly. For this to work, only given instances that are type compatible with the parameter will be considered.

The details for the new idioms and the reasons for their existence are explained in AbstractingOverContextPart1.

Nesting Method Definitions and Recursion

Method definitions can also be nested. This is useful when you want to refactor a lengthy method body into smaller methods, but the “helper” methods aren’t needed outside the original method. Nesting them inside the original method means they are invisible to the rest of the code base, including other methods in the type.

Here is an example for a factorial calculator:

// src/script/scala/progscala3/typelessdomore/Factorial.scala

def factorial(i: Int): Long =
  def fact(i: Int, accumulator: Long): Long =
    if (i <= 1) accumulator
    else fact(i - 1, i * accumulator)

  fact(i, 1L)

(0 to 5).foreach(i => println(s"$i: ${factorial(i)}"))

The last line prints the following:

0: 1
1: 1
2: 2
3: 6
4: 24
5: 120

The fact method calls itself recursively, passing an accumulator parameter, where the result of the calculation is “accumulated.” Note that we return the accumulated value when the counter i reaches 1. (We’re ignoring negative integer arguments, which would be invalid. The function just returns 1 for i <= 1.) After the definition of the nested method, factorial calls it with the passed-in value i and the initial accumulator “seed” value of 1.

Notice that we use i as a parameter name twice, first in the factorial method and again in the nested fact method. The use of i as a parameter name for fact “shadows” the outer use of i as a parameter name for factorial. This is fine, because we don’t need the outer value of i inside fact. We only use it the first time we call fact, at the end of factorial.

Like a local variable declaration in a method, a nested method is also only visible inside the enclosing method.

Look at the return types for the two functions. We used Long because factorials grow in size quickly. So, we didn’t want Scala to infer Int as the return type. Otherwise, we don’t need the type annotation on factorial.

However, we must declare the return type for fact, because it is recursive and Scala’s local-scope type inference can’t infer the return type of recursive functions.

You might be a little nervous about a recursive function. Aren’t we at risk of blowing up the stack? The JVM and many other language environments don’t do tail-call optimizations, which would convert a tail-recursive function into a loop. This prevents stack overflow and also makes execution faster by eliminating the additional function invocations.

The term tail-recursive means that the recursive call is the last thing done in an expression and only one recursive call is made. If we made the recursive call, then added something to the result, for example, that would not be a tail call. This doesn’t mean that non-tail-call recursion is disallowed, just that we can’t optimize it into a loop.

Recursion is a hallmark of functional programming and a powerful tool for writing elegant implementations of many algorithms. Hence, the Scala compiler does limited tail-call optimizations itself. It will handle functions that call themselves, but not so-called “trampoline” calls, i.e., “a calls b calls a calls b,” etc.

Still, you might want to know if you got it right and the compiler did in fact perform the optimization. No one wants a blown stack in production. Fortunately, the compiler can tell you if you got it wrong, if you add an annotation, tailrec, as shown in this refined version of factorial:

// src/script/scala/progscala3/typelessdomore/FactorialTailrec.scala
import scala.annotation.tailrec

def factorial(i: Int): Long =
  @tailrec
  def fact(i: Int, accumulator: Long): Long =
    if i <= 1 then accumulator
    else fact(i - 1, i * accumulator)

  fact(i, 1)

(0 to 5).foreach(i => println(s"$i: ${factorial(i)}"))

If fact is not actually tail recursive, the compiler will throw an error. Consider this attempt to write a naïve recursive implementation of Fibonacci sequences:

// src/script/scala/progscala3/typelessdomore/FibonacciTailrec.scala
scala> import scala.annotation.tailrec

scala> @tailrec
     | def fibonacci(i: Int): Long =
     |   if (i <= 1) 1L
     |   else fibonacci(i - 2) + fibonacci(i - 1)
4 |  else fibonacci(i - 2) + fibonacci(i - 1)
  |                          ^^^^^^^^^^^^^^^^
  |                 Cannot rewrite recursive call: it is not in tail position
4 |  else fibonacci(i - 2) + fibonacci(i - 1)
  |       ^^^^^^^^^^^^^^^^
  |       Cannot rewrite recursive call: it is not in tail position

We are attempting to make two recursive calls, not one, and then do something with the returned values, add them. So, this function is not tail recursive. (It’s naïve because it is possible to write a tail recursive implementation.)

Finally, the nested function can see anything in scope, including arguments passed to the outer function. Note the use of n in count in the next example:

// src/script/scala/progscala3/typelessdomore/CountTo.scala
import scala.annotation.tailrec

def countTo(n: Int): Unit =
  @tailrec
  def count(i: Int): Unit =
    if (i <= n) then
      println(i)
      count(i + 1)
  count(1)

countTo(5)

Inferring Type Information

Statically typed languages provide wonderful compile-time safety, but they can be very verbose if all the type information has to be explicitly provided. Scala’s type inference removes most of this explicit detail, but where it is still required, it can provide an additional benefit of documentation for the reader.

Some functional programming languages, like Haskell, can infer almost all types, because they do global type inference. Scala can’t do this, in part because Scala has to support subtype polymorphism, for object-oriented inheritance, which makes type inference harder.

We’ve already seen examples of Scala’s type inference. Here are two more examples, showing different ways to declare a Map:

scala> val map1: Map[Int, String] = Map.empty
val map1: Map[Int, String] = Map()

scala> val map2 = Map.empty[Int, String]
val map1: Map[Int, String] = Map()

The second form is more idiomatic most of the time. However, Map is actually a trait with concrete subclasses, so you’ll sometimes make declarations like this one for a TreeMap:

scala> import scala.collection.immutable.TreeMap
scala> val map3: Map[Int, String] = TreeMap.empty
val map3: Map[Int, String] = Map()

Here is a summary of the rules for when explicit type annotations are required in Scala.

The last case is somewhat rare, fortunately.

Note

The Any type is the root of the Scala type hierarchy. If a block of code is inferred to return a value of type Any unexpectedly, chances are good that the code is more general than you intended so that Any is the only common super type of all possible values.

We’ll explore Scala’s types in ScalaTypeHierarchy.

Let’s look at a few examples of cases we haven’t seen yet where explicit types are required. First, look at overloaded methods:

// src/script/scala/progscala3/typelessdomore/MethodOverloadedReturn.scala

case class Money(value: Double)
case object Money {
  def apply(s: String): Money     = apply(s.toDouble)
  def apply(d: BigDecimal): Money = apply(d.toDouble)
}

While the Money constructor expects a Double, we want the user to have the convenience of passing a String or a BigDecimal (ignoring possible errors). So, we add two more apply methods to the companion object. Both call the apply(d: Double) method the compiler automatically generates for the companion object, corresponding to the primary constructor Money(value: Double).

The two methods have explicit return types. If you try removing them, you’ll get a compiler error:

scala> case class Money(value: Double)
     | case object Money {
     |  def apply(s: String)     = apply(s.toDouble)  // no return type
     |  def apply(d: BigDecimal) = apply(d.toDouble)  // no return type
     | }
4 | def apply(d: BigDecimal) = apply(d.toDouble)
  |                            ^
  |                    Overloaded or recursive method apply needs return type
3 | def apply(s: String)     = apply(s.toDouble)
  |                            ^
  |                    Overloaded or recursive method apply needs return type

Variadic Argument Lists

Scala supports methods that take variable argument lists (sometimes just called variadic methods). Consider this contrived example that computes the mean of Doubless:

// src/script/scala/progscala3/typelessdomore/VariadicArguments.scala

scala> object Mean1 {
     |   def calc1(ds: Double*): Double = calc2(ds :_*)
     |   def calc2(ds: Seq[Double]): Double = ds.sum/ds.size
     | }

scala> Mean1.calc1(1.0, 2.0)
val res0: Double = 1.5

scala> Mean1.calc2(Seq(1.0, 2.0))
val res1: Double = 1.5

The syntax ds: Double* means zero or more Doubles, a variable argument list. Since calc1 calls calc2, which expects a Seq[Double] argument. the unusual syntax (ds :_*) is how you take the variable argument list and convert to a sequence when needed.

Why have both functions? The examples show that both can be convenient for the user. In particular, using calc1 doesn’t require you to wrap values in a Seq first.

There are downsides. The API “footprint” is larger with two methods instead of one and the maintenance burden is larger.

Assuming you want both methods, why not use the same name, in particular, apply?

scala> object Mean2 {
     |   def apply(ds: Double*): Double = apply(ds :_*)
     |   def apply(ds: Seq[Double]): Double = ds.sum/ds.size
     | }
3 |  def apply(ds: Seq[Double]): Double = ds.sum/ds.size
  |      ^
  |      Double definition:
  |      def apply(ds: Double*): Double in object Mean2 at line 2 and
  |      def apply(ds: Seq[Double]): Double in object Mean2 at line 3
  |      have the same type after erasure.

In fact, ds: Double*$$ is converted to a kind of sequence internally, so effectively the methods look identical at the byte code level.

There’s a common idiom to break the ambiguity, add a first Double parameter in the first apply, then use a variable list for the rest of the supplied arguments:

scala> object Mean {
     |   def apply(d: Double, ds: Double*): Double = apply(d +: ds)
     |   def apply(ds: Seq[Double]): Double = ds.sum/ds.size
     | }
// defined object Mean

scala> Mean(1.0,2.0)
val res10: Double = 1.5

scala> Mean(Seq(1.0,2.0))
val res11: Double = 1.5

scala> Mean()
1 |Mean()
  |^^^^
  |None of the overloaded alternatives of method apply in object Mean with types
  | (ds: Seq[Double]): Double
  | (d: Double, ds: Double*): Double
  |match arguments ()

scala> Mean(Nil)
val res12: Double = NaN

When calling the second apply, the first one constructs a new sequence prepending d to ds using d +: ds. We’ll explain this syntax in MatchingOnSequences.

Finally, Nil is an object representing an empty sequence with any type of elements.

Reserved Words

Scala reserves some words for defining constructs like conditionals, declaring variables, etc. Some of the reserved words are marked with (soft), which means they can be used as regular identifiers for method and variable names, for example, but they are treated as keywords when used in particular contexts. All of the soft words are new reserved words in Scala 3. The reason for treating them as soft is to avoid breaking older code that happens to use them as identifiers.

reserved-words-table lists the reserved keywords in Scala. Many are found in Java and they usually have the same meanings in both languages.

Table 2-1. Reserved words
Word Description See …

abstract

Makes a declaration abstract.

ClassBasics

as

(soft) Used with given.

context_bounds_1

case

Start a case clause in a match expression. Define a “case class.”

PatternMatching

catch

Start a clause for catching thrown exceptions.

TryCatchFinally

class

Start a class declaration.

ObjectOrientedProgramming

def

Start a method declaration.

MethodDeclarationsAndDefinitions

do

New syntax for while and for loops without braces. Old Scala 2 do…while loop.

_scala_while_loops

else

Start an else clause for an if clause.

ConditionalExpressions

extends

Indicates that the class or trait that follows is the parent type of the class or trait being declared.

ParentTypes

extension

(soft) Marks one or more extension methods for a type.

TypeClasses

false

Boolean false.

BooleanLiterals

final

Applied to a class or trait to prohibit deriving child types from it. Applied to a member to prohibit overriding it in a derived class or trait.

AvoidOverridingConcreteMembers

finally

Start a clause that is executed after the corresponding try clause, whether or not an exception is thrown by the try clause.

TryCatchFinally

for

Start a for comprehension (loop).

ForComprehensions

forSome

Used in Scala 2 for existential type declarations to constrain the allowed concrete types that can be used. Dropped in Scala 3.

ExistentialTypes

given

Marks implicit definitions.

AbstractingOverContextPart1

if

Start an if clause.

ConditionalExpressions

implicit

Marks a method or value as eligible to be used as an implicit type converter or value. Marks a method parameter as optional, as long as a type-compatible substitute object is in the scope where the method is called.

AbstractingOverContextPart1

import

Import one or more types or members of types into the current scope.

Importing

lazy

Defer evaluation of a val.

LazyVals

match

Start a pattern-matching clause.

PatternMatching

new

Create a new instance of a class.

ClassBasics

null

Value of a reference variable that has not been assigned a value.

ScalaTypeHierarchy

object

Start a singleton declaration: a class with only one instance.

ATasteOfScala

opaque

(soft) Declares a special type alias with zero runtime overhead.

OpaqueTypesAndValueClasses

open

(soft) Declares a concrete class is open for subclassing.

OpenVsClosed

override

Override a concrete member of a type, as long as the original is not marked final.

OverridingMembers

package

Start a package scope declaration.

Packages

private

Restrict visibility of a declaration.

VisibilityRules

protected

Restrict visibility of a declaration.

VisibilityRules

requires

Deprecated. Was used for self-typing.

SelfTypeAnnotations

return

Return from a function.

ATasteOfScala

sealed

Applied to a parent type. Requires all derived types to be declared in the same source file.

SealedClassHierarchies

super

Analogous to this, but binds to the parent type.

OverridingMethods

then

New syntax for if expressions

ConditionalExpressions

this

How an object refers to itself. The method name for auxiliary constructors.

Constructors

throw

Throw an exception.

TryCatchFinally

trait

A mixin module that adds additional state and behavior to an instance of a class. Can also be used to just declare methods, but not define them, like a Java interface.

Traits

try

Start a block that may throw an exception.

TryCatchFinally

true

Boolean true.

BooleanLiterals

type

Start a type declaration.

AbstractTypesVsParameterizedTypes

using

(soft) Scala 3 alternative to implicit for implicit arguments.

AbstractingOverContextPart1

val

Start a read-only “variable” declaration.

VariableDeclarationsAndDefinitions

var

Start a read-write variable declaration.

VariableDeclarationsAndDefinitions

while

Start a while loop.

OtherLoopingConstructs

with

Include the trait that follows in the class being declared or the object being instantiated.

Traits

yield

Return an element in a for comprehension that becomes part of a sequence.

Yielding

_

A placeholder, used in imports, function literals, etc.

Many

:

Separator between identifiers and type annotations.

ATasteOfScala

=

Assignment.

ATasteOfScala

=>

Used in function literals to separate the parameter list from the function body.

AnonymousFunctionsLambdasAndClosures

<-

Used in for comprehensions in generator expressions.

ForComprehensions

<:

Used in parameterized and abstract type declarations to constrain the allowed types.

TypeBounds

<%

Used in parameterized and abstract type “view bounds” declarations.

ViewBounds

>:

Used in parameterized and abstract type declarations to constrain the allowed types.

TypeBounds

#

Used in type projections.

TypeProjections

@

Marks use of an annotation.

Annotations

Some Java methods use names that are reserved words by Scala, for example, java.util.Scan⁠ner.match. To avoid a compilation error, surround the name with single back quotes (“back ticks”), e.g., java.util.Scanner.`match`.

Literal Values

We’ve seen a few literal values already, such as val book = "Programming Scala", where we initialized a val book with a String literal, and (s: String) => s.toUpperCase, an example of a function literal. Let’s discuss all the literals supported by Scala.

Numeric Literals

Scala 3 expands the ways that numeric literals can be written and used as initializers. Consider these examples:

val i: Int = 123                       // decimal
val x: Long = 0x123L                   // hexadecimal (291 decimal)
val f: Float = 123_456.789F            // 123456.789
val d: Double = 123_456_789.0123       // 123456789.0123
val y: BigInt = 0x123_a4b_c5d_e6f_789  // 82090347159025545
val z: BigDecimal = 123_456_789.0123   // 123456789.0123

Scala 3 allows underscores to make long numbers easier to read. They can appear anywhere in the literal (except between 0x), not just between every third character.

Hexadecimal numbers start with 0x followed by one or more digits and the letters a through f and A through F.

Indicate a negative number by prefixing the literal with a sign.

The ability to use numeric literals for library and user-defined types like BigInt and BigDecimal is implemented with a new trait called FromDigits.

For Long literals, it is necessary to append the L or l character at the end of the literal, unless you are assigning the value to a variable declared to be Long. Otherwise, Int is inferred. The valid values for an integer literal are bounded by the type of the variable to which the value will be assigned. integer-boundaries-table defines the limits, which are inclusive.

Table 2-2. Ranges of allowed values for integer literals (boundaries are inclusive)
Target type Minimum (inclusive) Maximum (inclusive)

Long

−263

263 − 1

Int

−231

231 − 1

Short

−215

215 − 1

Char

0

216 − 1

Byte

−27

27 − 1

A compile-time error occurs if an integer literal number is specified that is outside these ranges.

Floating-point literals are expressions with an optional minus sign, zero or more digits and underscores, followed by a period (.), followed by one or more digits. For Float literals, append the F or f character at the end of the literal. Otherwise, a Double is assumed. You can optionally append a D or d for a Double.

Floating-point literals can be expressed with or without exponentials. The format of the exponential part is e or E, followed by an optional + or , followed by one or more digits.

Here are some example floating-point literals, where Double is inferred unless the declared variable is Float or an f or F suffix is used:

0.14
3.14
3.14f
3.14F
3.14d
3.14D
3e5
3E5
3.14e+5
3.14e-5
3.14e-5f
3.14e-5F
3.14e-5d
3.14e-5D

At least one digit must appear after the period, 3. and 3.e5 are disallowed. Use 3.0 and 3.0e5 instead. Otherwise it would be ambiguous; do you mean method e5 on the Int value of 3 or do you mean floating point literal 3.0e5?

Float consists of all IEEE 754 32-bit, single-precision binary floating-point values. Double consists of all IEEE 754 64-bit, double-precision binary floating-point values.

Boolean Literals

The Boolean literals are true and false. The type of the variable to which they are assigned will be inferred to be Boolean:

scala> val b1 = true
b1: Boolean = true

scala> val b2 = false
b2: Boolean = false

Character Literals

A character literal is either a printable Unicode character or an escape sequence, written between single quotes. A character with a Unicode value between 0 and 255 may also be represented by an octal escape, i.e., a backslash () followed by a sequence of up to three octal characters. It is a compile-time error if a backslash character in a character or string literal does not start a valid escape sequence.

Here are some examples:

'A'
'u0041'  // 'A' in Unicode
'
'
'012'    // '
' in octal
'	'

The valid escape sequences are shown in char-escape-sequences-table.

Table 2-3. Character escape sequences
Sequence Meaning



Backspace (BS)

Horizontal tab (HT)

Line feed (LF)

f

Form feed (FF)

Carriage return (CR)

"

Double quote (")

Single quote ()

\

Backslash ()

Note that nonprintable Unicode characters like u0009 (tab) are not allowed. Use the equivalents like . Recall that three Unicode characters were mentioned in reserved-words-table as valid replacements for corresponding ASCII sequences, ⇒ for =>, → for ->, and ← for <-.

String Literals

A string literal is a sequence of characters enclosed in double quotes or triples of double quotes, i.e., """…""".

For string literals in double quotes, the allowed characters are the same as the character literals. However, if a double quote (") character appears in the string, it must be “escaped” with a character. Here are some examples:

"Programming
Scala"
"He exclaimed, "Scala is great!""
"First	Second"

The string literals bounded by triples of double quotes are also called multiline string literals. These strings can cover several lines; the line feeds will be part of the string. They can include any characters, including one or two double quotes together, but not three together. They are useful for strings with characters that don’t form valid Unicode or escape sequences, like the valid sequences listed in char-escape-sequences-table. Regular expressions are a good example, which use lots of escaped characters with special meanings. Conversely, if escape sequences appear, they aren’t interpreted.

Here are three example strings:

"""Programming
Scala"""
"""He exclaimed, "Scala is great!" """
"""First line

Second line	

Fourth line"""

Note that we had to add a space before the trailing """ in the second example to prevent a parse error. Trying to escape the second " that ends the "Scala is great!" quote, i.e., "Scala is great!", doesn’t work.

When using multiline strings in code, you’ll want to indent the substrings for proper code formatting, yet you probably don’t want that extra whitespace in the actual string output. String.stripMargin solves this problem. It removes all whitespace in the substrings up to and including the first occurrence of a vertical bar, |. If you want some whitespace indentation, put the whitespace you want after the |. Consider this example:

// src/script/scala/progscala3/typelessdomore/MultilineStrings.scala

def hello(name: String) = s"""Welcome!
  Hello, $name!
  * (Gratuitous Star!!)
  |We're glad you're here.
  |  Have some extra whitespace.""".stripMargin

val hi = hello("Programming Scala")

The last line prints the following:

val hi: String = Welcome!
  Hello, Programming Scala!
  * (Gratuitous Star!!)
We're glad you're here.
  Have some extra whitespace.

Note where leading whitespace is removed and where it isn’t.

If you want to use a different leading character than |, use the overloaded version of stripMargin that takes a Char (character) parameter. If the whole string has a prefix or suffix you want to remove (but not on individual lines), there are corresponding stripPrefix and stripSuffix methods, too:

scala> "<hello> <world>".stripPrefix("<").stripSuffix(">")
val res0: String = hello> <world

The < and > inside the string are not removed.

Symbol Literals

Scala 2 supported symbols, which are interned strings, meaning that two symbols with the same “name” (i.e., the same character sequence) will actually refer to the same object in memory. They are deprecated, but still exist in Scala 3. However, the literal syntax has been removed:

scala> val sym1 = 'name             // Scala 2 only; single "tick"
scala> val sym2 = Symbol("name")    // Scala 2 and 3

You might see symbols in older code, but don’t use them yourself.

Function Literals

As we’ve seen already, (i: Int, d: Double) => (i+d).toString is a function literal of type Function2[Int,Double,String], where the last type is the return type.

You can even use the literal syntax for a type declaration. The following declarations are equivalent:

val f1: (Int,String) => String       = (i, s) => s+i
val f2: Function2[Int,String,String] = (i, s) => s+i

Tuple Literals

Often, declaring a class to hold instances with two or more values is more than you need. You could put those values in a collection, but then you lose their specific type information. Scala implements tuples of values, where the individual types are retained. A literal syntax for tuples uses a comma-separated list of values surrounded by parentheses.

Here is an example of a tuple declaration and how we can access elements and use pattern matching to extract them:

// src/script/scala/progscala3/typelessdomore/TupleExample.scala

scala> val tup = ("Hello", 1, 2.3)               1
val tup: (String, Int, Double) = (Hello,1,2.3)

scala> val tup2: (String, Int, Double) = ("World", 4, 5.6)
val tup2: (String, Int, Double) = (World,4,5.6)

scala> tup._1                                    2
val res0: String = Hello

scala> tup._2
val res1: Int = 1

scala> tup._3
val res2: Double = 2.3

scala> val (s, i, d) = tup                       3
val s: String = Hello
val i: Int = 1
val d: Double = 2.3

scala> println(s"s = $s, i = $i, d = $d")
s = Hello, i = 1, d = 2.3
1

Use the literal syntax to construct a three-element tuple. Note the literal syntax is used for the type, too.

2

Extract the first element of the tuple. Tuple indexing is one-based, by historical convention, not zero-based. The next two lines extract the second and third elements.

3

Declare three values, s, i, and d, that are assigned the three corresponding fields from the tuple using pattern matching.

Two-element tuples, sometimes called pairs for short, are so commonly used there is a special syntax for constructing them:

scala> (1, "one")
val res3: (Int, String) = (1,one)

scala> 1 -> "one"
val res4: (Int, String) = (1,one)

scala> Tuple2(1, "one")           // Rarely used.
val res5: (Int, String) = (1,one)

For example, maps are often constructed with key-value pairs as follows:

// src/script/scala/progscala3/typelessdomore/StateCapitalsSubset.scala

scala> val stateCapitals = Map(
     |   "Alabama" -> "Montgomery",
     |   "Alaska"  -> "Juneau",
     |   // ...
     |   "Wyoming" -> "Cheyenne")
val stateCapitals: Map[String, String] =
  Map(Alabama -> Montgomery, Alaska -> Juneau, Wyoming -> Cheyenne)

Option, Some, and None: Avoiding nulls

Let’s discuss three useful types that express a very useful concept, when we may or may not have a value.

Most languages have a special keyword or type instance that’s assigned to reference variables when there’s nothing else for them to refer to. In Scala and Java, it’s called null.

Using null is a giant source of nasty bugs. What null really signals is that we don’t have a value in a given situation. If the value is not null, we do have a value. Why not express this situation explicitly with the type system and exploit type checking to avoid NullPointerExceptions?

Option lets us express this situation explicitly without using null. Option is an abstract class and its two concrete subclasses are Some, for when we have a value, and None, when we don’t.

You can see Option, Some, and None in action using the map of state capitals in the United States that we declared in the previous section:

scala> stateCapitals.get("Alabama")
     | stateCapitals.get("Wyoming")
     | stateCapitals.get("Unknown")
val res6: Option[String] = Some(Montgomery)
val res7: Option[String] = Some(Cheyenne)
val res8: Option[String] = None

scala> stateCapitals.getOrElse("Alabama", "Oops1")
     | stateCapitals.getOrElse("Wyoming", "Oops2")
     | stateCapitals.getOrElse("Unknown", "Oops3")
val res9: String = Montgomery
val res10: String = Cheyenne
val res11: String = Oops3

Map.get returns an Option[T], where T is String in this case. Either a Some wrapping the value is returned or a None when no value for the specified key was found.

In contrast, similar methods in other languages just return a T value, when found, or null.

By returning an Option, we can’t “forget” that we have to verify that something was returned. In other words, the fact that a value may not exist for a given key is enshrined in the return type for the method declaration. This also provides clear documentation for the user of Map.get about what can be returned.

The second group uses Map.getOrElse. This method returns either the value found for the key or it returns the second argument passed in, which functions as the default value to return.

So, getOrElse is more convenient, as you don’t need to process the Option, if a suitable default value exists.

To reiterate, because the Map.get method returns an Option, it automatically documents for the reader that there may not be an item matching the specified key. The map handles this situation by returning a None.

Also, thanks to Scala’s static typing, you can’t make the mistake of “forgetting” that an Option is returned and attempting to call a method supported by the type of the value inside the Option. You must extract the value first or handle the None case. Without an option return type, when a method just returns a value, it’s easy to forget to check for null before calling methods on the returned object.

When You Can’t Avoid Nulls

Because Scala runs on the JVM, JavaScript, and native environments, it must interoperate with other libraries, which means Scala has to support null.

Scala 3 introduces a new way to indicate a possible null through the type Scala.Null, which is a subtype of all other types. Suppose you have a Java HashMap to access:

// src/script/scala/progscala3/typelessdomore/Null.scala

import java.util.{HashMap => JHashMap}                     1

val jhm = new JHashMap[String,String]()
jhm.put("one", "1")

val one1: String = jhm.get("one")                          2
val one2: String | Null = jhm.get("one")                   3

val two1: String = jhm.get("two")                          4
val two2: String | Null = jhm.get("two")
1

Import the Java HashMap, but give it an alias so it doesn’t shadow Scala’s HashMap.

2

Return the string “1”.

3

Declare explicitly that one2 is of type String or Null. The value will still be “1” in this case.

4

These two values will equal null.

Scala 3 introduces union types, which we use for one2 and two2. They tell the reader that the value could be either a String or a null.

There is an optional feature to enable aggressive null checking. If you add the flag -Yexplicit-nulls, then the declarations of one1 and two1 will be disallowed, because the compiler knows you are referring to a Java library where null could be returned. I have not enabled this option in the code examples build, because it forces lots of changes to otherwise safe code and it has other implications that are described in more detail in the documentation. However, if you use this same code in a REPL with this flag, you’ll see the following:

$ scala -Yexplicit-nulls
...
scala> val one1: String = jhm.get("one")
1 |val one1: String = jhm.get("one")
  |                   ^^^^^^^^^^^^^^
  |                   Found:    String | UncheckedNull
  |                   Required: String

...

Tony Hoare, who invented the null reference in 1965 while working on a language called ALGOL W, called its invention his “billion dollar” mistake. Use Option instead. Consider enabling explicit nulls.

Sealed Class Hierarchies and Enumerations

While we’re discussing Option, let’s discuss a useful design feature it uses. A key point about Option is that there are really only two valid subtypes. Either we have a value, the Some case, or we don’t, the None case. There are no other subtypes of Option that would be valid. So, we would really like to prevent users from creating their own.

Scala 2 and 3 have a keyword sealed for this purpose. Option could be declared as follows:

sealed abstract class Option[+A] {...}
final case class Some[+A](a: A) extends Option[A] {...}
final case object None extends Option[Nothing] {...}

The sealed keyword tells the compiler that all subclasses must be declared in the same source file. Some and None are declared in the same file with Option in the Scala library. This technique effectively prevents additional subtypes of Option.

None has an interesting declaration. It is a case class with only one instance, so it is declared case object. The Nothing type along with the Null type are subtypes of all other types in Scala. We’ll explain Nothing in more detail in SeqsInFunctionalProgramming, if you get what I mean…

You can also declare a type final if you want to prevent users from subtyping it. If you want to encourage subtyping of a concrete class, you can declare it open. (This is redundant for abstract types.)

This same constraint on subclassing can now be achieved more concise in Scala 3 with the new enum syntax:

enum Option[+A] {
  case Some(a: A) {...}
  case None {...}
  ...
}

Organizing Code in Files and Namespaces

Scala adopts the package concept that Java uses for namespaces, but Scala offers more flexibility. Filenames don’t have to match the type names and the package structure does not have to match the directory structure. So, you can define packages in files independent of their “physical” location.

The following example defines a class MyClass in a package com.example.mypkg using the conventional Java syntax:

// src/main/scala/progscala3/typelessdomore/PackageExample1.scala
package com.example.mypkg

class MyClass:
  def mymethod(s: String): String = s

Scala also supports a block-structured syntax for declaring package scope:

// src/main/scala/progscala3/typelessdomore/PackageExample2.scala
package com:
  package example:
    package pkg1:
      class Class11:
        def m = "m11"

      class Class12:
        def m = "m12"

    package pkg2:
      class Class21:
        def m = "m21"
        def makeClass11 = new pkg1.Class11

        def makeClass12 = new pkg1.Class12

    package pkg3.pkg31.pkg311:
      class Class311:
        def m = "m21"

Two packages, pkg1 and pkg2, are defined under the com.example package. A total of three classes are defined between the two packages. The makeClass11 and makeClass12 methods in Class21 illustrate how to reference a type in the “sibling” package, pkg1. You can also reference these classes by their full paths, com.example.pkg1.Class11 and com.example.pkg1.Class12, respectively.

The package pkg3.pkg31.pkg311 shows that you can “chain” several packages together in one statement. It is not necessary to use a separate package statement for each package.

If you have package-level declarations, like types, in each of several parent packages that you want to bring into scope, use separate package statements as shown for each level of the package hierarchy with these declarations. Each subsequent package statement is interpreted as a subpackage of the previously specified package, as if we used the block-structure syntax shown previously. The first statement is interpreted as an absolete path.

Following the convention used by Java, the root package for Scala’s library classes is named scala.

Although the package declaration syntax is flexible, one limitation is that packages cannot be defined within classes and objects, which wouldn’t make much sense anyway.

Warning

Scala does not allow package declarations in scripts, which are implicitly wrapped in an object, where package declarations are not permitted.

Importing Types and Their Members

To use declarations in packages, you have to import them. However, Scala offers flexible options how items are imported:

import java.awt._                 1
import java.io.File               2
import java.io.File._             3
import java.util.{Map, HashMap}   4
1

Import everything in a package, using underscore ( _ ) as a wildcard.

2

Import an individual type.

3

Import all static member fields and methods in File.

4

Selectively import two types from java.util.

Java uses the asterisk character (*) as the wildcard for imports. In Scala, this character is allowed as a method name (e.g., for multiplication), so _ is used instead to avoid ambiguity.

The third line imports all the static methods and fields in java.io.File. The equivalent Java import statement would be import static java.io.File.*;. Scala doesn’t have an import static construct because it treats object types uniformly like other types.

You can put import statements almost anywhere, so you can scope their visibility to just where they are needed, you can rename types as you import them, and you can suppress the visibility of unwanted types:

def stuffWithBigInteger() = {

  import java.math.BigInteger.{
    ONE => _,                     1
    TEN,                          2
    ZERO => JAVAZERO }            3

  // println( "ONE: "+ONE )     // ONE is effectively undefined
  println( "TEN: "+TEN )
  println( "ZERO: "+JAVAZERO )
}
1

Alias to _ to make it invisible. Use this technique when you want to import everything except a few items.

2

Import TEN from BigDecimal. It can be referenced simply as TEN.

3

Import ZERO but give it an alias. Use this technique to avoid shadowing other items with the same name. This is used a lot when mixing Java and Scala types, such as Java’s List and Scala’s List.

Because this import statement is inside stuffWithBigInteger, the imported items are not visible outside the function.

Finally, Scala 3 adds new ways to control how implicit definitions are imported. We’ll discuss these details in GivensAndImports, once we understand the new syntax and behaviors for given instances.

Package Imports and Package Objects

Sometimes it’s nice to give the user one import statement for a public API that brings in all types, as well as constants and methods not attached to a type. For example:

import progscala3.typelessdomore.api._

This is simple to do; just define anything you need under the package:

// src/main/scala/progscala3/typelessdomore/PackageObjects.scala
package progscala3.typelessdomore.api

val DEFAULT_COUNT = 5
def countTo(limit: Int = DEFAULT_COUNT) = (0 to limit).foreach(println)

class Class1:
  def m = "cm1"

object Object1:
  def m = "om1"

In Scala 2, non-type definitions had to be declared inside a package object, like this:

// src/main/scala-2/progscala3/typelessdomore/PackageObjects.scala
package progscala3.typelessdomore   // Notice, no ".api"

package object api {
	val DEFAULT_COUNT = 5
	def countTo(limit: Int = DEFAULT_COUNT) = (0 to limit).foreach(println)

	class Class1 {
	  def m = "cm1"
	}

	object Object1 {
	  def m = "om1"
	}
}

Package objects are still supported in Scala 3, but they are deprecated.

Abstract Types Versus Parameterized Types

We mentioned in ATasteOfScala that Scala supports parameterized types, which are very similar to generics in Java. (The terms are somewhat interchangeable, but the Scala community uses parameterized types.) Java uses angle brackets (<…>), while Scala uses square brackets ([…]), because < and > are often used for method names.

Because we can plug in almost any type for a type parameter A in a collection like List[A], this feature is called parametric polymorphism, because generic implementations of the List methods can be used with instances of any type A.

Consider the declaration of Map, which is written as follows, where K is the keys type and V is the values type.

trait Map[K, +V] extends Iterable[(K, V)] with ...

The + in front of the V means that Map[K, V2] is a subtype of Map[K, V1] for any V2 that is a _subtype of V1. This is called covariant typing. It is a reasonably intuitive idea. If we have a function f(map: Map[String, Any]), it makes sense that passing a Map[String, Double] to it should work fine, because the function has to assume values of Any, a parent type of Double.

In contrast, the key K is invariant. We can’t pass Map[Any, Any] to f nor any Map[S, Any] for some subtype or supertype S of String.

If there is a in front of a type parameter, the relationship goes the other way; Foo[B] would be a supertype of Foo[A], if B is a subtype of A and the declaration is Foo[-A] (called contravariant typing). This is less intuitive, but also not as important to understand now. We’ll see how it is important for function types in ParameterizedTypes.

Scala supports another type abstraction mechanism called abstract types, which can be applied to many of the same design problems for which parameterized types are used. However, while the two mechanisms overlap, they are not redundant. Each has strengths and weaknesses for certain design problems.

These types are declared as members of other types, just like methods and fields. Here is an example that uses an abstract type in a parent class, then makes the type member concrete in child classes:

// src/main/scala/progscala3/typelessdomore/AbstractTypes.scala
package progscala3.typelessdomore
import scala.io.Source

abstract class BulkReader:
  type In                                                            1
  val source: In
  /** Read source and return a sequence of Strings */
  def read: Seq[String]

case class StringBulkReader(source: String) extends BulkReader:      2
  type In = String
  def read: Seq[String] = Seq(source)

case class FileBulkReader(source: Source) extends BulkReader:        3
  type In = Source
  def read: Seq[String] = source.getLines.toVector
1

Abstract type, really just like any abstract field or method.

2

Concrete subtype of BulkReader where In is defined to be String. Note that the type of the source parameter must match.

3

Concrete subtype of BulkReader where In is defined to be Source, the Scala library class for reading sources, like files. Source.getLines returns an iterator, which we can easy read into a Vector with toVector.

Strictly speaking, we don’t need to declare the source field in the parent class, but I put it there to show you that the concrete case classes can make it a constructor parameter, where the specific type is specified.

Using these readers:

scala> import progscala3.typelessdomore.{StringBulkReader, FileBulkReader}

scala> new StringBulkReader("Hello Scala!").read
val res1: Seq[String] = List(Hello Scala!)

scala> val lines = FileBulkReader(Source.fromFile("README.md")).read
val lines: Seq[String] = Vector(# Programming Scala, 3rd Edition, ...)

scala> lines(0)    // look at two lines...
     | lines(2)
val res2: String = # Programming Scala, 3rd Edition
val res3: String = ## README for the Code Examples

The type field is used like a type parameter in a parameterized type. In fact, as an exercise, try rewriting the example to use type parameters, e.g., BulkReader[In].

So what’s the advantage here of using type members instead of parameterized types? The latter are best for the case where the type parameter has no relationship with the parameterized type, like List[A] when A is Int, String, Person, etc. A type member works best when it “evolves” in parallel with the enclosing type, as in our BulkReader example, where the type member needed to match the “behaviors” expressed by the enclosing type, specifically the read method. Sometimes this characteristic is called family polymorphism or covariant specialization.

For completeness, another use for type members is to provide a convenient alias for a more complicated type. For example, suppose you use (String, Double) tuples a lot in some code. You could either declare a class for it or use a type alias for a simple alternative:

scala> object Foo:
     |   type Record = (String, Double)
     |   def transform(record: Record): Record =
     |     (record._1.toUpperCase, 2*record._2)
// defined object Foo

scala> Foo.transform("hello", 10)
val res0: Foo.Record = (HELLO,20.0)

Notice the type shown for res0. In fact, concrete type members are always type aliases.

Recap and What’s Next

We covered a lot of practical ground, such as literals, keywords, file organization, and imports. We learned how to declare variables, methods, and classes. We learned about Option as a better tool than null, plus other useful techniques. In the next chapter, we will finish our fast tour of the Scala “basics” before we dive into more detailed explanations of Scala’s features.

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

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