Chapter 3. Rounding Out the Basics

Let’s finish our survey of essential “basics” in Scala.

Operator Overloading?

Almost all “operators” are actually methods. Consider this most basic of examples:

1 + 2

The plus sign between the numbers is a method on the Int type.

Scala doesn’t have special “primitives” for numbers and booleans that are distinct from types you define. They are regular types: Float, Double, Int, Long, Short, Byte, Char, and Boolean. Hence, they can have methods. However, Scala will compile to native platform primitives when possible for greater efficiency.

Also we’ve already seen that Scala identifiers can have most nonalphanumeric characters and single-parameter methods can be invoked with infix operator notation. So, 1 + 2 is the same as 1.+(2).

Caution

Actually, they don’t always behave identically, due to operator precedence rules. While 1 + 2 * 3 = 7, 1.+(2)*3 = 9. When present, the period binds before the star.

In fact, you aren’t limited to “operator-like” method names when using infix notation. It is common to write code like the following, where the dot and parentheses around println are omitted:

scala> Seq("one", "two") foreach println
one
two

Use this convention cautiously, as these expressions can sometimes be confusing to read or parse.

To bring a little more discipline to this practice, Scala 3 now issue a deprecation warning if a one-parameter method is declared without the annotation @infix, but is used with infix notation. However, to support traditional practice, most of the familiar collection “operators”, like map, foreach, etc. are declared with @infix, so our foreach example still works in Scala 3 without a warning.

If you still want to use infix notation and you want to avoid the deprecation warning for a method that isn’t annotated with @infix, put the name in “back ticks”:

// src/script/scala/progscala3/rounding/InfixMethod.scala

case class Foo(str: String):
  def append(s: String): Foo = copy(str + s)

Foo("one").append("two")        1
Foo("one") append "two"         2
Foo("one") `append` "two"       3
1

Normal usage.

2

Triggers a deprecation warning.

3

Accepted usage, but odd looking.

You can also define your own operator methods with symbolic names. Suppose you want to allow users to create java.io.File objects by appending strings using /, the file separator for UNIX-derived systems. Consider the following implementation:

// src/main/scala/progscala3/rounding/Path.scala
package progscala3.rounding

import scala.annotation.alpha
import java.io.File

case class Path(
    value: String, separator: String = Path.defaultSeparator):  1
  val file = new File(value)
  override def toString: String = file.getPath                  2

  @alpha("concat") def / (node: String): Path =                 3
    copy(value + separator + node)                              4

object Path:
  val defaultSeparator = sys.props("file.separator")
1

Use the operating systems default path separator string as the default separator when constructing a File object.

2

How to override the default toString method. Here, I use the path string from File.

3

I’ll explain the @alpha in a moment.

4

Use the case class copy method to create a new instance, changing only the value.

Now users can create new File objects as follows:

scala> import progscala3.rounding.Path

scala> val one  = Path("one")
val one: progscala3.rounding.Path = one

scala> val three = one / "two" / "three"
val three: progscala3.rounding.Path = one/two/three

scala> three.file
val res0: java.io.File = one/two/three

scala> val threeb = one./("two")./("three")
val threeb: progscala3.rounding.Path = one/two/three

scala> three == threeb
val res1: Boolean = true

scala> one `concat` "two"
1 |one `concat` "two"
  |^^^^^^^^^^^^
  |value concat is not a member of progscala3.rounding.Path

On Windows, the character would be used as the default separator. This method is designed to be used with infix notation. It looks odd to use normal invocation syntax.

In Scala 3, the @alpha annotation is recommended for methods with symbolic names. It will be required in a future release of Scala 3. In this example, concat is the name the compiler will use internally when it generates byte code.

This is the name you would use if you wanted to call the method from Java code, which doesn’t support invoking methods with symbolic names. However, the name concat can’t be used in Scala code, as shown at the end of the example. It only affects the byte code produced by the compiler.

The @infix annotation is not required for methods that use “operator” characters, like * and /, because support for symbolic operators has always existed for the particular purpose of allowing intuitive, infix expressions, like a * b and path1 / path2.

Types can also be written with infix notation, in many contexts. The same rules using the @infix annotation apply:

// src/script/scala/progscala3/rounding/InfixType.scala
import scala.annotation.{alpha, infix}

@alpha("BangBang") case class !![A,B](a: A, b: B)     1
val ab1: Int !! String = 1 !! "one"                   2
val ab2: Int !! String = !!(1, "one")                 3

@infix case class bangbang[A,B](a: A, b: B)           4
val ab1: Int bangbang String = 1 bangbang "one"
val ab2: Int bangbang String = bangbang(1, "one")
1

Some type declaration with two type parameters.

2

An attempt to use infix notation on both sides, but we get an error that !! is not a method on Int. We’ll solve this problem in AbstractingOverContextPart1.

3

This declaration works, with the non-infix notation on the right-hand side.

4

These three lines behave the same, but note the use of @infix now.

To recap:

  • Annotate symbolic operator definitions with @alpha("some_name").

  • Annotate alphanumeric types and methods with @infix if you want to allow their use with infix notation.

Related to infix notation is postfix notation, where a method with no parameters can be invoked without the period. However, postfix invocations can sometimes be even more ambiguous and confusing, so Scala requires that you explicitly enable this feature first, in one of several ways:

  1. Add the import statement import scala.language.postfixOps to the source code.

  2. Invoke the compiler with the option -language:postfixOps “feature flag”.

There is no @postfix annotation.

Tip

The SBT build for the examples is configured to use the -feature flag that enables warnings when features are used, like postfix expressions, that should be enabled explicitly.

Dropping the punctuation by using infix or even postfix expresses can make your code cleaner and help create elegant programs that read more naturally, but avoid cases that are actually harder to understand.

Allowed Characters in Identifiers

Here is a summary of the rules for characters in identifiers for method and type names, variables, etc:

Characters

Scala allows all the printable ASCII characters, such as letters, digits, the underscore ( _ ), and the dollar sign ($), with the exceptions of the “parenthetical” characters, (, ), [, ], {, and }, and the “delimiter” characters, `, , ', ", ., ;, and ,. Scala allows the characters between u0020 and u007F that are not in the sets just shown, such as mathematical symbols, the so-called operator characters like / and <, and some other symbols. This includes white space characters, discussed below.

Reserved words can’t be used

We listed the reserved words in ReservedWords. Recall that some of them are combinations of operator and punctuation characters. For example, a single underscore ( _ ) is a reserved word!

Plain identifiers—combinations of letters, digits, $, _, and operators

A plain identifier can begin with a letter or underscore, followed by more letters, digits, underscores, and dollar signs. Unicode-equivalent characters are also allowed. Scala reserves the dollar sign for internal use, so you shouldn’t use it in your own identifiers, although this isn’t prevented by the compiler. After an underscore, you can have either letters and digits, or a sequence of operator characters. The underscore is important. It tells the compiler to treat all the characters up to the next whitespace as part of the identifier. For example, val xyz_++= = 1 assigns the variable xyz_++= the value 1, while the expression val xyz++= = 1 won’t compile because the “identifier” could also be interpreted as xyz ++=, which looks like an attempt to append something to xyz. Similarly, if you have operator characters after the underscore, you can’t mix them with letters and digits. This restriction prevents ambiguous expressions like this: abc_-123. Is that an identifier abc_-123 or an attempt to subtract 123 from abc_?

Plain identifiers—operators

If an identifier begins with an operator character, the rest of the characters must be operator characters.

“Back-quote” literals

An identifier can also be an arbitrary string between two back quote characters, e.g., def `test that addition works` = assert(1 + 1 == 2). (Using this trick for literate test names is the one use I can think of for this otherwise-questionable technique for using white space in identifiers.) Also use back quotes to invoke a method or variable in a Java class when the name is identical to a Scala reserved word, e.g., java.net.Proxy.‵type‵().

Pattern-matching identifiers

In pattern-matching expressions (for example, ASampleApplication), tokens that begin with a lowercase letter are parsed as variable identifiers, while tokens that begin with an uppercase letter are parsed as constant identifiers (such as class names). This restriction prevents some ambiguities because of the very succinct variable syntax that is used, e.g., no val keyword is present.

Syntactic Sugar

Once you know that all operators are methods, it’s easier to reason about unfamiliar Scala code. You don’t have to worry about special cases when you see new operators. We’ve seen several examples where infix expressions like matrix1 * matrix2 where used, which are actually just ordinary method invocations.

This flexible method naming gives you the power to write libraries that feel like a natural extension of Scala itself. You can write a new math library with numeric types that accept all the usual mathematical operators. The possibilities are constrained by just a few limitations for method names.

Caution

Avoid making up operator symbols when an established ASCII name exists, because the latter is easier to understand and remember, especially for beginners reading your code.

Methods with Empty Parameter Lists

Scala is flexible about the use of parentheses in methods with no parameters.

If a method takes no parameters, you can define it without parentheses. Callers must invoke the method without parentheses. Conversely, if you add empty parentheses to your definition, callers must add the parentheses.

For example, List.size has no parentheses, so you write List(1, 2, 3).size. If you try List(1, 2, 3).size(), you’ll get an error.

However, exceptions are made for no-parameter methods in Java libraries. For example, the length method for java.lang.String does have parentheses in its definition, because Java requires them, but Scala lets you write both "hello".length() and "hello".length. This flexibility with Scala-defined methods was also allowed in earlier releases of Scala, but Scala 3 now requires that usage match the definition.

A convention in the Scala community is to omit parentheses for no-parameter methods that have no side effects, like returning the size of a collection, which could actually be a precomputed, immutable field in the object. So, many no-parameter methods could be interpreted as simply reading a field. However, when the method performs side effects or does extensive work, parentheses are added by convention, providing a hint to the reader of nontrivial activity, for example myFileReader.readLines().

Why bother with optional parentheses in the first place? They make some method call chains read better as expressive, self-explanatory “sentences” of code:

// src/script/scala/progscala3/rounding/NoDotBetter.scala

def isEven(n: Int) = (n % 2) == 0

Seq(1, 2, 3, 4) filter isEven foreach println

The second line is “clean”, but on the edge of non-obvious to read. Here is the same code repeated four times with progressively less explicit detail filled in. The last line is the original:

Seq(1, 2, 3, 4).filter((i: Int) => isEven(i)).foreach((i: Int) => println(i))
Seq(1, 2, 3, 4).filter(i => isEven(i)).foreach(i => println(i))
Seq(1, 2, 3, 4).filter(isEven).foreach(println)
Seq(1, 2, 3, 4) filter isEven foreach println

The first three versions are more explicit and hence better for the beginning reader to understand. However, once you’re familiar with the fact that filter is a method on collections that takes a single argument, foreach loops over a collection, and so forth, the last, “Spartan” implementation is much faster to read and understand. The other two versions have more visual noise that just get in the way, once you’re more experienced. Keep that in mind as you learn to read Scala code.

To be clear, this expression works because each method we used took a single parameter. If you tried to use a method in the chain that takes zero or more than one parameter, it would confuse the compiler. In those cases, put some or all of the punctuation back in.

The previous explanation glossed over an important detail. The four versions are not exactly the same code with different details inferred by the compiler, although all four behave the same way. Consider the function arguments passed to filter in the second and third examples. They are different as follows:

  • filter(i => isEven(i)) passes an anonymous function that calls isEven.

  • filter(isEven) passes isEven itself as the function. Note that isEven is a named function.

The same distinction applies to the functions passed to foreach in the second and third examples.

Precedence Rules

So, if an expression like 2.0 * 4.0 / 3.0 * 5.0 is actually a series of method calls on Doubles, what are the operator precedence rules? Here they are in order from lowest to highest precedence:

  1. All letters

  2. |

  3. ^

  4. &

  5. < >

  6. = !

  7. :

  8. + -

  9. * / %

  10. All other special characters

Characters on the same line have the same precedence. An exception is = when it’s used for assignment, in which case it has the lowest precedence.

Because * and / have the same precedence, the two lines in the following scala session behave the same:

scala> 2.0 * 4.0 / 3.0 * 5.0
res0: Double = 13.333333333333332

scala> (((2.0 * 4.0) / 3.0) * 5.0)
res1: Double = 13.333333333333332

Left vs. Right Associative Methods

Usually, method invocation using infix operator notation simply bind in left-to-right order, i.e., they are left-associative. Don’t all methods work this way? No. In Scala, any method with a name that ends with a colon (:) binds to the right when used in infix notation, while all other methods bind to the left. For example, you can prepend an element to a Seq using the +: method (sometimes called “cons,” which is short for “constructor,” a term introduced by Lisp):

scala> val seq = Seq('b', 'c', 'd')
val seq: Seq[Char] = List(b, c, d)

scala> val seq2 = 'a' +: seq
val seq2: Seq[Char] = List(a, b, c, d)

scala> val seq3 = 'z'.+:(seq2)
1 |val seq3 = 'z'.+:(seq2)
  |           ^^^^^^
  |           value +: is not a member of Char

scala> val seq3 = seq2.+:('z')
val seq3: Seq[Char] = List(z, a, b, c, d)

Note that if we don’t use infix notation, we have to put seq2 on the left.

Tip

Any method whose name ends with a : binds to the right, not the left, in infix operator notation.

Enumerations and Algebraic Data Types

While it’s common to declare a type hierarchy to represent all the possible “kinds” of some abstraction, sometimes we know the list of them is fixed and mostly what we need are unique “flags” to indicate each one.

Take for example the days of the week, where we have seven fixed values. An enumeration for the English days of the week can be declared as follows:

enum WeekDay(val fullName: String):                             1
  case Sun extends WeekDay("Sunday")                            2
  case Mon extends WeekDay("Monday")
  case Tue extends WeekDay("Tuesday")
  case Wed extends WeekDay("Wednesday")
  case Thu extends WeekDay("Thursday")
  case Fri extends WeekDay("Friday")
  case Sat extends WeekDay("Saturday")

  def isWorkingDay: Boolean = ! (this == Sat || this == Sun)    3

import WeekDay._

val sorted = WeekDay.values.sortBy(_.ordinal).toSeq             4
assert(sorted == List(Sun, Mon, Tue, Wed, Thu, Fri, Sat))

assert(Sun.fullName == "Sunday")
assert(Sun.ordinal == 0)                                        5
assert(Sun.isWorkingDay == false)
assert(WeekDay.valueOf("Sun") == WeekDay.Sun)                   6
1

Declare an enumeration, similar to declaring a class. You can have optional fields as shown. Declare them with val if you want them to be accessible, e.g., WeekDay.Sun.fullName. The derives Eql clause lets use do comparisons with == and !=. We’ll explain this construct in TypeClassDerivation and MultiversalEquality.

2

The values are declared using the case keyword.

3

You can define methods.

4

The WeekDay.values order does not match the declaration order, so we sort by the ordinal.

5

The ordinal value matches the declaration order.

6

You can lookup a enumeration value by its name.

Scala 3 introduced a new syntax for enumerations, which we saw briefly in SealedClassHierarchies.1 The new syntax lends itself to a more concise definition of algebraic data types (ADTs - not to be confused with abstract data types). An ADT is ``algebraic` in the sense that transformations obey well defined properties (think of addition with integers as an example), such as transforming an element or combining two of them with an operation can only yield another element in the set.

Consider the following example:

// src/script/scala/progscala3/rounding/TreeADT.scala

object Scala2ADT:
  sealed trait Tree[T]                                          1
  final case class Branch[T](                                   2
    left: Tree[T], right: Tree[T]) extends Tree[T]
  final case class Leaf[T](elem: T) extends Tree[T]             3

  val tree = Branch(
    Branch(
      Leaf(1),
      Leaf(2)),
    Branch(
      Leaf(3),
      Branch(Leaf(4),Leaf(5))))

object Scala3ADT:
  enum Tree[T]:                                                 4
    case Branch(left: Tree[T], right: Tree[T])
    case Leaf(elem: T)

  import Tree._                                                 5
  val tree = Branch(
    Branch(
      Leaf(1),
      Leaf(2)),
    Branch(
      Leaf(3),
      Branch(Leaf(4),Leaf(5))))

Scala2ADT.tree                                                  6
Scala3ADT.tree
1

Use a sealed type hierarchy. Valid for Scala 2 and 3.

2

One subtype, a branch with left and right children.

3

The other subtype, a leaf node.

4

Scala 3 syntax using the new enum construct. It is much more concise.

5

The elements of the enum need to be imported.

6

Is the output the same for these two lines?

We saw sealed type hierarchies before in SealedClassHierarchies. The enum syntax provides the same benefits as sealed type hierarchies, but with much less code.

The types of the tree values are slightly different:

scala> Scala2ADT.tree
val res1: Scala2ADT.Branch[Int] = Branch(...)

scala> Scala3ADT.tree
val res2: Scala3ADT.Tree[Int] = Branch(...)

One last point; you may have noticed that Branch and Leaf don’t extend Tree, while in WeekDay above, each day extends WeekDay. For Branch and Leaf, extending Tree is inferred by the compiler, although we could add this explicitly. For WeekDay, each day must extend WeekDay to provide a value for the val fullName: String field declared by WeekDay.

Interpolated Strings

We introduced interpolated strings in ASampleApplication. Let’s explore them further.

A String of the form s"foo ${bar}" will have the value of expression bar, converted to a String and inserted in place of ${bar}. If the expression bar returns an instance of a type other than String, a toString method will be invoked, if one exists. It is an error if it can’t be converted to a String.

If bar is just a variable reference, the curly braces can be omitted. For example:

val name = "Buck Trends"
println(s"Hello, $name")

When using interpolated strings, to write a literal dollar sign $, use two of them, $$.

There are two other kinds of interpolated strings. The first kind provides printf formatting and uses the prefix f. The second kind is called “raw” interpolated strings. It doesn’t expand escape characters, like .

Suppose we’re generating financial reports and we want to show floating-point numbers to two decimal places. Here’s an example:

val gross   = 100000F
val net     = 64000F
val percent = (net / gross) * 100
println(f"$$${gross}%.2f vs. $$${net}%.2f or ${percent}%.1f%%")

The output of the last line is the following:

$100000.00 vs. $64000.00 or 64.0%

Scala uses Java’s Formatter class for printf formatting. The embedded references to expressions use the same ${…} syntax as before, but printf formatting directives trail them with no spaces.

In this example, we use two dollar signs, $$, to print a literal US dollar sign, and two percent signs, %%, to print a literal percent sign. The expression ${gross}%.2f formats the value of gross as a floating-point number with two digits after the decimal point.

The types of the variables used must match the format expressions, but some implicit conversions are performed. An Int expression in a floating point context is allowed. It just pads with zeros. However, attempting to use Double or Float in an Int context causes a compilation error, due to the truncation that would be required.

While Scala uses Java strings, in certain contexts the Scala compiler will wrap a Java String with extra methods defined in scala.collection.StringOps. One of those extra methods is an instance method called format. You call it on the format string itself, then pass as arguments the values to be incorporated into the string. For example:

scala> val s = "%02d: name = %s".format(5, "Dean Wampler")
val s: String = "05: name = Dean Wampler"

In this example, we asked for a two-digit integer, padded with leading zeros.

The final version of the built-in string interpolation capabilities is the “raw” format that doesn’t expand control characters. Consider these examples:

scala> val name = "Dean Wampler"
val name: String = "Dean Wampler"

scala> val multiLine = s"123
$name
456"
val multiLine: String = 123
Dean Wampler
456

scala> val multiLineRaw = raw"123
$name
456"
val multiLineRaw: String = 123nDean Wamplern456

Finally, we can actually define our own string interpolators, but we’ll need to learn more about context abstractions first. See BuildYourOwnStringInterpolator for details.

Scala Conditional Expressions

Scala conditionals start with the if keyword. They are expressions, meaning they return a value that you can assign to a variable. In many languages, if conditionals are statements, which can only perform side effect operations.

Scala 2 if expressions used braces just like Java’s if statements, but in Scala 3, if expressions can also use the new conventions discussed in the previous section. A simple example:

// src/script/scala/progscala3/rounding/If.scala

(0 until 6) foreach { n =>
  if n%2 == 0 then
    println(s"$n is even")
  else if n%3 == 0 then
    println(s"$n is divisible by 3")
  else
    println(n)
}

Recall from NewScala3Syntax that the then keyword is required only if you pass the -new-syntax flag to the compiler or REPL, which we use in the code examples SBT file. However, if you don’t use that flag, you must wrap the predicate expressions, like n%2 == 0, in parentheses.

The bodies of each clause are so concise, we can write them on the same line as the if or else expressions:

// src/script/scala/progscala3/rounding/If2.scala

(0 until 6) foreach { n =>
  if n%2 == 0 then println(s"$n is even")
  else if n%3 == 0 then println(s"$n is divisible by 3")
  else println(n)
}

Here are the same examples using the curly brace syntax required by Scala 2 and optional for Scala 3:

// src/script/scala-2/progscala3/rounding/If.scala

(0 until 6) foreach { n =>
  if (n%2 == 0) {
    println(s"$n is even")
  } else if (n%3 == 0) {
    println(s"$n is divisible by 3")
  } else {
    println(n)
  }
}
// src/script/scala-2/progscala3/rounding/If2.scala

(0 until 6) foreach { n =>
  if (n%2 == 0) println(s"$n is even")
  else if (n%3 == 0) println(s"$n is divisible by 3")
  else println(n)
}

The older syntax can still be used, even with compiler flags for the new syntax, but you can also use flags to require the older syntax, as discussed in NewScala3Syntax.

What is the type of the returned value if objects of different types are returned by different branches? The type of the returned value will be the so-called least upper bound (LUB) of all the branches, the closest parent type that matches all the potential values from each clause.

In the following example, the LUB is Option[String], because the three branches return either Some[String] or None. The returned sequence is of type IndexedSeq[Option[String]]:

// src/script/scala/progscala3/rounding/IfTyped.scala

scala> val seq = (0 until 6) map { n =>
     |   if n%2 == 0 then Some(n.toString)
     |   else None
     | }
val seq: IndexedSeq[Option[String]] = Vector(Some(0), None, Some(2), ...)

Conditional Operators

Scala uses the same conditional operators as Java. conditional-operators provides the details.

Table 3-1. Conditional operators
Operator Operation Description

&&

and

The values on the left and right of the operator are true. The righthand side is only evaluated if the lefthand side is true.

||

or

At least one of the values on the left or right is true. The righthand side is only evaluated if the lefthand side is false.

>

greater than

The value on the left is greater than the value on the right.

>=

greater than or equals

The value on the left is greater than or equal to the value on the right.

<

less than

The value on the left is less than the value on the right.

<=

less than or equals

The value on the left is less than or equal to the value on the right.

==

equals

The value on the left is the same as the value on the right.

!=

not equals

The value on the left is not the same as the value on the right.

The && and || operators are “short-circuiting”. They stop evaluating expressions as soon as the answer is known. This is handy when you must work with null values:

scala> val s: String|Null = null
val s: String | Null = null

scala> val okay = s != null && s.length > 5
val okay: Boolean = false

Calling s.length would throw a NullPointerException without the s != null test first. What happens if you use || instead? Also, we don’t us if here, because we just want to know the Boolean value of the conditional.

Most of the operators behave as they do in Java and other languages. An exception is the behavior of == and its negation, !=. In Java, == compares instance references only. It doesn’t check logical equality, i.e., comparing field values. You must call the equals method for that purpose. Instead in Scala, == and != call the equals method for the left-hand instance. You actually don’t implement equals yourself very often in Scala, because you mostly only compare case class instances and the compiler generates equals automatically for case classes! You can override it when you need to, however.

If you need to determine if two instances are identical references, use the eq method.

See EqualityOfInstances for more details.

Scala for Comprehensions

Another familiar control structure that’s particularly feature-rich in Scala is the for loop, called the for comprehension. They are expressions, not statements as in Java.

The term comprehension comes from functional programming. It expresses the idea that we are traversing one or more collections of some kind, “comprehending” something new from it, such as another collection. Python list comprehensions are a similar concept.

for Loops

Let’s start with a basic for expression. As for if expressions, I use the new format options consistently in the code examples, except where noted.

// src/script/scala/progscala3/rounding/BasicFor.scala

for
  i <- 0 until 10
do println(i)

Since there is one expression inside the for … do, you can put the expression on the same line after the for and you can even put everything on one line:

for i <- 0 until 10
do println(i)

for i <- 0 until 10 do println(i)

As you might guess, this code says, “For every integer between 0 inclusive and 10 exclusive, print it on a new line.”

Because this form doesn’t return anything, it only performs side effects. These kinds of for comprehensions are sometimes called for loops, analogous to Java for loops.

In the older Scala 2 syntax, this example would be written as follows:

// src/script/scala-2/progscala3/rounding/BasicFor.scala

for (i <- 0 until 10)
  println(i)

for (i <- 0 until 10) println(i)
Tip

From now on, in the examples that follow, I’ll only show Scala 3 syntax, but you can find Scala 2 versions of some examples, in the code examples under the directory src/*/scala-2/progscala3/... and a table of differences in OldVsNewSyntax.

Generator Expressions

The expression i <- 0 until 10 is called a generator expression, so named because it generates individual values in some way. The left arrow operator (<-) is used to iterate through any collection or iterator instance that supports sequential access, such as Seq and Vector.

Guards: Filtering Values

We can add if expressions, called guards, to filter for just elements we want to keep:

// src/script/scala/progscala3/rounding/GuardFor.scala

for
  n <- 0 to 6
  if n%2 == 0
do println(n)

The output is the number 0, 2, 4, and 6 (because we use to to make the 6 inclusive). Note the sense of filtering; the guards express what to keep, not remove.

Yielding New Values

So far our for loops have only performed side effects, writing to output. Usually, we want to return a new collection, making our for expressions comprehensions rather than loops. We use the yield keyword to express this intent:

// src/script/scala/progscala3/rounding/YieldingFor.scala

val evens = for
  n <- 0 to 10  // Note: 0 to 10, inclusive
  if n%2 == 0
yield n

assert(evens == Seq(0, 2, 4, 6, 8, 10))

Each iteration through the for expression “yields” a new value, named n. These are accumulated into a new collection that is assigned to the variable evens.

The type of the collection resulting from a comprehension expression is inferred from the type of the collection being iterated over. Seq is a trait and the actual concrete instance returned is of type IndexedSeq.

In the following example, a Vector[Int] is converted to a Vector[String]:

// src/script/scala/progscala3/rounding/YieldingForVector.scala

val odds = for
  number <- Vector(1,2,3,4,5)
  if number % 2 == 1
yield number.toString

assert(odds == Vector("1", "3", "5"))

Expanded Scope and Value Definitions

You can define immutable values inside the for expressions without using the val keyword, like fn in the following example that uses the WeekDay enumeration we defined earlier in this chapter:

// src/script/scala/progscala3/rounding/ScopedFor.scala

import progscala3.rounding.WeekDay

val days = for
  day <- WeekDay.values
  if day.isWorkingDay
  fn = day.fullName
yield fn

assert(days.toSeq ==
	Seq("Friday", "Monday", "Tuesday", "Wednesday", "Thursday"))

In this case, the for comprehension now returns an Array[String], because WeekDay.values returns an Array[WeekDay]. Because Arrays are Java Arrays and Java doesn’t define a useful equals method, we convert to a Seq with toSeq and perform the assertion check.

Now let’s consider a powerful use of Option with for comprehensions. Recall we discussed Option as a better alternative to using null. It’s also useful to recognize that Option is a special kind of collection, limited to zero or one elements. In fact, we can “comprehend” it too:

// src/script/scala/progscala3/rounding/ScopedOptionFor.scala

import progscala3.rounding.WeekDay
import progscala3.rounding.WeekDay._

val dayOptions = Seq(
  Some(Mon), None, Some(Tue), Some(Wed), None,
  Some(Thu), Some(Fri), Some(Sat), Some(Sun), None)

val goodDays1 = for          // First pass
  dayOpt <- dayOptions
  day <- dayOpt
  fn   = day.fullName
yield fn
assert(goodDays1 == Seq("Monday", "Tuesday", "Wednesday", ...))

val goodDays2 = for          // second, more concise pass
  case Some(day) <- dayOptions
  fn = day.fullName
yield fn
assert(goodDays1 == Seq("Monday", "Tuesday", "Wednesday", ...))

Imagine that we called some services to return days of the week. The services returned Options, because some of the services couldn’t return anything, so they returned None. Now we want to remove and ignore the None values.

In the first expression of the first for comprehension, each element extracted is an Option, assigned to dayOpt. The next line uses the arrow to extract the value in the option and assign it to day.

But wait! Doesn’t None throw an exception if you try to extract a value from it? Yes, but the comprehension effectively checks for this case and skips the Nones. It’s as if we added an explicit if dayOpt != None before the second line.

Hence, we construct a collection with only values from Some instances.

The second for comprehension makes this filtering even cleaner and more concise, using pattern matching. The expression case Some(day) <- dayOptions only succeeds when the instance is a Some, also skipping the None values, and it extracts the value into to day, all in one step.

To recap the difference between using the left arrow (<-) versus the equals sign (=), use the arrow when you are iterating through a collection and extracting values. Use the equals sign when you’re assigning a value from another value that doesn’t involve iteration. A limitation is that the first expression in a for comprehension has to be an extraction/iteration using the left arrow. If you really need to define a value first, put it before the for comprehension.

When working with loops in many languages, they provide break and continue keywords for breaking out of a loop completely or continuing to the next iteration, respectively. Scala doesn’t have either of these keywords, but when writing idiomatic Scala code, they aren’t missed. Use conditional expressions to test if a loop should continue, or make use of recursion. Better yet, filter your collections ahead of time to eliminate complex conditions within your loops.

Scala while Loops

The while loop is seldom used. It executes a block of code as long as a condition is true:

// src/script/scala/progscala3/rounding/While.scala

var count = 0
while count < 10
  count += 1
  println(count)

assert(count == 10)

Scala do-while Loops

Scala 3 dropped the do-while construct in Scala 2, because it was rarely used. It can be rewritten using while, although awkwardly:

// src/script/scala/progscala3/rounding/DoWhileAlternative.scala

var count = 0
while
  count += 1
  println(count)
  count < 10
do ()
assert(count == 10)

Using try, catch, and finally Clauses

Through its use of functional constructs and strong typing, Scala encourages a coding style that lessens the need for exceptions and exception handling. But exceptions are still supported. In particular, they are needed when using Java libraries.

Unlike Java, Scala does not have checked exceptions. Java’s checked exceptions are treated as unchecked by Scala. There is also no throws clause on method declarations. However, there is a @throws annotation that is useful for Java interoperability. See Annotations.

Scala uses pattern matching to specify the exceptions to be caught.

The following example implements a common application scenario, resource management. We want to open files and process them in some way. In this case, we’ll just count the lines. However, we must handle a few error scenarios. The file might not exist, perhaps because the user misspelled the filenames. Also, something might go wrong while processing the file. (We’ll trigger an arbitrary failure to test what happens.) We need to ensure that we close all open file handles, whether or not we process the files successfully:

// src/main/scala/progscala3/rounding/TryCatch.scala
package progscala3.rounding
import scala.io.Source                                               1
import scala.util.control.NonFatal

/** Usage: scala rounding.TryCatch filename1 filename2 ... */
@main def TryCatch(fileNames: String*) =                             2
  fileNames foreach { fileName =>
    var source: Option[Source] = None                                3
    try                                                              4
      source = Some(Source.fromFile(fileName))                       5
      val size = source.get.getLines.size
      println(s"file $fileName has $size lines")
    catch
      case NonFatal(ex) => println(s"Non fatal exception! $ex")      6
    finally
      for s <- source do                                             7
        println(s"Closing $fileName...")
        s.close
  }
1

Import scala.io.Source for reading input and scala.util.control.NonFatal for matching on “nonfatal” exceptions, i.e., those where it’s reasonable to attempt recovery.

2

In Scala 3, we don’t need an object to wrap the “main” method. By using the @main annotation, we can name the method whatever we want and we can specify the number and types of the arguments expected. Here, we just expect zero or more strings.

3

Declare the source to be an Option, so we can tell in the finally clause if we have an actual instance or not. We use a mutable variable, but it’s hidden inside the implementation and thread safety isn’t a concern here.

4

Start of try clause.

5

Source.fromFile will throw a java.io.FileNotFoundException if the file doesn’t exist. Otherwise, wrap the returned Source instance in a Some. Calling get on the next line is safe, because if we’re here, we know we have a Some.

6

Catch nonfatal errors. For example, out of memory would be fatal.

7

Use a for comprehension to extract the Source instance from the Some and close it. If source is None, then nothing happens.

Note the catch clause. Scala uses pattern matches to pick the exceptions you want to catch. This is more compact and more flexible than Java’s use of separate catch clauses for each exception. In this case, the clause case NonFatal(ex) => … uses scala.util.control.NonFatal to match any exception that isn’t considered fatal.

The finally clause is used to ensure proper resource cleanup in one place. Without it, we would have to repeat the logic at the end of the try clause and the catch clause, to ensure our file handles are closed. Here we use a for comprehension to extract the Source from the option. If the option is actually a None, nothing happens; the block with the close call is not invoked. Note that since this is “main” method, the handles would be cleaned up anyway on exit, but you’ll want to close resources in other contexts.

Tip

When resources need to be cleaned up, whether or not the resource is used successfully, put the cleanup logic in a finally clause.

This program is already compiled by sbt and we can run it from the sbt prompt using the runMain task, which lets us pass arguments. I have elided some output:

> runMain progscala3.rounding.TryCatch README.md foo/bar
file README.md has 116 lines
Closing README.md...
Non fatal exception! java.io.FileNotFoundException: foo/bar (...)

You throw an exception by writing throw new MyBadException(…). If your custom exception is a case class, you can omit the new.

Automatic resource management is a common pattern. Let’s use a Scala library facility, scala.util.Using for this purpose.2 Then we’ll actually implement our own version to illustrate some powerful capabilities in Scala and better understand how the library version works.

// src/main/scala/progscala3/rounding/FileSizes.scala
package progscala3.rounding

import scala.util.Using
import scala.io.Source

/** Usage: scala rounding.FileSizes filename1 filename2 ... */
@main def FileSizes(fileNames: String*) =
  val sizes = fileNames map { fileName =>
    Using.resource(Source.fromFile(fileName)) { source =>
      source.getLines.size
    }
  }
  println(s"Returned sizes: ${sizes.mkString(", ")}")
  println(s"Total size: ${sizes.sum}")

This simple program also counts the number of lines in the files specified on the command line. However, if a file is not readable or doesn’t exist, an exception is thrown and processing stops. No other results are produced, unlike the previous TryCatch example, which continues processing the arguments specified.

See the scala.util.Using documentation for a few other ways this utility can be used. For more sophisticated approaches to error handling, see Nedelcu2020,Alexandru Nedelcu's blog.

Call by Name, Call by Value

Now let’s implement our own application resource manager to learn a few powerful techniques that Scala provides for us. This implementation will build on the TryCatch example:

// src/main/scala/progscala3/rounding/TryCatchArm.scala
package progscala3.rounding
import scala.language.reflectiveCalls
import reflect.Selectable.reflectiveSelectable
import scala.util.control.NonFatal
import scala.io.Source

object manage:
  def apply[R <: { def close():Unit }, T](resource: => R)(f: R => T): T =
    var res: Option[R] = None
    try
      res = Some(resource)         // Only reference "resource" once!!
      f(res.get)                   // Return the T instance
    catch
      case NonFatal(ex) =>
        println(s"manage.apply(): Non fatal exception! $ex")
        throw ex
    finally
      res match
        case Some(resource) =>
          println(s"Closing resource...")
          res.get.close()
        case None => // do nothing

/** Usage: scala rounding.TryCatchARM filename1 filename2 ... */
@main def TryCatchARM(fileNames: String*) =
  val sizes = fileNames map { fileName =>
    try
      val size = manage(Source.fromFile(fileName)) { source =>
        source.getLines.size
      }
      println(s"file $fileName has $size lines")
      size
    catch
      case NonFatal(ex) =>
        println(s"caught $ex")
        0
  }
  println("Returned sizes: " + (sizes.mkString(", ")))

The output will be similar what we saw for TryCatch.

This is a lovely little bit of separation of concerns, but to implement it, we used a few new power tools.

First, we named our object manage rather than Manage. Normally, you follow the convention of using a leading uppercase letter for type names, but in this case we will use manage like a function. We want client code to look like we’re using a built-in operator, similar to a while loop. This is another example of Scala’s tools for building little DSLs.

That manage.apply method declaration is hairy looking. Let’s deconstruct it. Here is the signature again, spread over several lines and annotated:

def apply[
  R <: { def close():Unit },   1
  T ]                          2
  (resource: => R)             3
  (f: R => T) = {...}          4
1

Two new things are shown here. R is the type of the resource we’ll manage. The <: means R is a subclass of something else. In this case, any type used for R must contain a close():Unit method. We declare this using a structural type defined with the braces. What would be more intuitive, especially if you are new to structural types, would be for all resources to implement a Closable interface that defines a close():Unit method. Then we could say R <: Closable. Instead, structural types let us use reflection and plug in any type that has a close():Unit method (like Source). Reflection has a lot of overhead and structural types are a bit scary, so reflection is another optional feature, like postfix expressions, which we saw earlier. So we add the import statements to tell the compiler we know what we’re doing.

2

T will be the type returned by the anonymous function passed in to do work with the resource.

3

It looks like resource is a function with an unusual declaration. Actually, resource is a by-name parameter, which we first encountered in ATasteOfFutures.

4

Finally we have a second parameter list containing a function for the work to do with the resource. This function will take the resource as an argument and return a result of type T.

Recapping point 1, here is how the apply method declaration would look if we could assume that all resources implement a Closable abstraction:

object manage {
  def apply[R <: Closable, T](resource: => R)(f: R => T) =
    ...
}

The line, res = Some(resource), is the only place resource is evaluated, which is important, because it is a by-name parameter. We learned in ATasteOfFutures that they are lazily evaluated, only when used, but they are evaluated every time they are referenced, just like a function call would be. The thing we pass as resource inside TryCatchARM, Source.fromFile(fileName), should only be evaluated once inside apply to construct the Source for a file. The code correctly evaluates it once.

So, you have to use by-name parameters carefully, but their virtue is the ability to control when and even if a block of code is evaluated. We’ll see another example shortly where we will evaluate a by-name parameter repeatedly for a good reason.

To recap, it’s as if the res = … line is actually this:

res = Some(Source.fromFile(fileName))

After constructing res, it is passed to the work function f.

See how manage is used in TryCatchARM. It looks like a built-in control structure with one parameter list that creates the Source and a second parameter list that is a block of code that works with the Source. So, using manage looks something like a conventional while statement.

Like most languages, Scala normally uses call-by-value semantics. If we write val source = Source.fromFile(fileName), it is evaluated immediately.

Supporting idiomatic code like our use of manage is the reason that Scala offers by-name parameters, without which we would have to pass an anonymous function that looks ugly:

manage(() => Source.fromFile(fileName)) { source =>

Then, within manage.apply, our reference to resource would now be a function call:

val res = Some(resource())

Okay, that’s not a terrible burden, but call by name enables a syntax for building our own control structures, like our manage utility.

Here is another example using a call by name, this time repeatedly evaluating two by-name parameters; an implementation of a while-like loop construct, called continue:

// src/script/scala/progscala3/rounding/CallByName.scala
import scala.annotation.tailrec

@tailrec                                                             1
def continue(conditional: => Boolean)(body: => Unit): Unit =         2
  if conditional then                                                3
    body
    continue(conditional)(body)

var count = 0
continue (count < 5) {                                               4
  println(s"at $count")
  count += 1
}
assert(count == 5)
1

Ensure the implementation is tail recursive.

2

Define a continue function that accepts two argument lists. The first list takes a single, by-name parameter that is the conditional. The second list takes a single, by-name value that is the body to be evaluated for each iteration.

3

Evaluate the condition. If true, evaluate the body and call continue recursively.

4

Try it with traditional brace syntax.

It’s important to note that the by-name parameters are evaluated every time they are referenced. So, by-name parameters are in a sense lazy, because evaluation is deferred, but possibly repeated over and over again. Scala also provides lazy values. By the way, this implementation shows how “loop” constructs can be replaced with recursion.

Unfortunately, this ability to define our own control structures only works with the old Scala 2 syntax using parentheses and braces. If continue really behaved like while or similar built-in constructs, we would be able to write the last two examples. The syntax uniformity with user-defined constructs is a nice feature we must give up if we use the new syntax… or do we??

count = 0
continue (count < 5)
  println(s"at $count")
  count += 1
assert(count == 5)

This actually parses, but it executes in an unexpected way. Here it is again, annotated to explain what actually happens:

continue (count < 5)     // Returns a "partially-applied function"
  println(s"at $count")  // A separate statement that prints "at 0" once
  count += 1             // Increments count once

A partially-applied function is one where we provide some, but not all arguments, returning a new function that will accept the remaining arguments (see PartiallyAppliedFunctionsVsPartialFunctions). Here’s what the REPL will print for that line:

scala> continue (count < 5)
val res1: (=> Unit) => Unit = Lambda$11103/0x00000008048c0040@4d0f9ec0

This is an anonymous function, implemented with a JDK lambda on the JDK, that takes a by-name parameter returning Unit (recall the second parameter for continue) and then returns Unit.

The second and third lines are not treated as part of the body that should be passed to continue. They are treated as single statements that are evaluated once.

So, the optional braces don’t work.

lazy val

By-name parameters show us that lazy evaluation is useful, but they are evaluated every time they are referenced.

There are times when you want to evaluate an expression once to initialize a field in an object, but you want to defer that invocation until the value is actually needed. In other words, on-demand evaluation. This is useful when:

  • The expression is expensive (e.g., opening a database connection) and you want to avoid the overhead until the value is actually needed, which could be never.

  • You want to improve startup times for modules by deferring work that isn’t needed immediately.

  • A field in an object needs to be initialized lazily so that other initializations can happen first.

We’ll explore the last scenario when we discuss InitializingFields.

Here is a “sketch” of an example using a lazy val:

// src/script/scala/progscala3/rounding/LazyInitVal.scala

case class DBConnection():
  println("In constructor")
  type MySQLConnection = String
  lazy val connection: MySQLConnection =
    // Connect to the database
    println("Connected!")
    "DB"

The lazy keyword indicates that evaluation will be deferred until the value is accessed.

Let’s try it. Notice when the println statements are executed:

scala> val dbc = DBConnection()
In constructor
val dbc: DBConnection = DBConnection()

scala> dbc.connection
Connected!
val res4: dbc.MySQLConnection = DB

scala> dbc.connection
val res5: dbc.MySQLConnection = DB

So, how is a lazy val different from a method call? We see that “Connected!” was only printed once, whereas if connection were a method, the body would be executed every time and we would see “Connected!” printed each time. Furthermore, we didn’t see that message until when we referenced connection the first time.

One-time evaluation makes little sense for a mutable field. Therefore, the lazy keyword is not allowed on vars.

Lazy values are implemented with the equivalent of a guard. When client code references a lazy value, the reference is intercepted by the guard to check if initialization is required. This guard step is really only essential the first time the value is referenced, so that the value is initialized first before the access is allowed to proceed. Unfortunately, there is no easy way to eliminate these checks for subsequent calls. So, lazy values incur overhead that “eager” values don’t. Therefore, you should only use lazy values when initialization is expensive, especially if the value may not actually be used. There are also some circumstances where careful ordering of initialization dependencies is most easily implemented by making some values lazy (see InitializingFields).

There is a @threadUnsafe annotation you can add to a lazy val. It causes the initialization to use a faster mechanism which is not thread-safe, so use with caution.

Traits: Interfaces and “Mixins” in Scala

Until now, I have emphasized the power of functional programming in Scala. I waited until now to discuss Scala’s features for object-oriented programming, such as how abstractions and concrete implementations are defined, and how inheritance is supported. We’ve seen some details in passing, like abstract and case classes and objects, but now it’s time to cover these concepts.

Scala uses traits to define abstractions. We’ll explore most details in Traits, but for now, think of them as interfaces for declaring abstract member fields, methods, and types, with the option of defining any or all of them, too.

Traits enable true separation of concerns and composition of behaviors (“mixins”).

Here is a typical enterprise developer task, adding logging. Let’s start with a service:

// src/script/scala/progscala3/rounding/Traits.scala
import util.Random

class Service(name: String):
  def work(i: Int): (Int, Int) =
    (i, Random.between(0, 1000))

val service1 = new Service("one")
(1 to 3) foreach (i => println(s"Result: ${service1.work(i)}"))

We ask the service to do some (random) work and get this output:

Result: (1,975)
Result: (2,286)
Result: (3,453)

Now we want to mix in a standard logging library. For simplicity, we’ll just use println.

Here are two traits, one that defines the abstraction with no concrete members and the other that implements the abstraction for “logging” to standard output:

trait Logging:
  def info   (message: String): Unit
  def warning(message: String): Unit
  def error  (message: String): Unit

trait StdoutLogging extends Logging:
  def info   (message: String) = println(s"INFO:    $message")
  def warning(message: String) = println(s"WARNING: $message")
  def error  (message: String) = println(s"ERROR:   $message")

Note that Logging is pure abstract. It works exactly like a Java interface. It is even implemented the same way in JVM byte code.

Finally, let’s declare a service that “mixes in” logging and use it:

val service2 = new Service("two") with StdoutLogging:
  override def work(i: Int): (Int, Int) =
    info(s"Starting work: i = $i")
    val result = super.work(i)
    info(s"Ending work: result = $result")
    result

(1 to 3) foreach (i => println(s"Result:  ${service2.work(i)}"))

We override the work method to log when we enter and before we leave the method. Scala requires the override keyword when you override a concrete method in a parent class. This prevents mistakes when you didn’t know you were overriding a method, for example from a library parent class and it catches misspelled method names that aren’t actually overrides! Note how we access the parent class work method, using super.work.

Here is the output:

INFO:    Starting work: i = 1
INFO:    Ending work: result = (1,737)
Result:  (1,737)
INFO:    Starting work: i = 2
INFO:    Ending work: result = (2,310)
Result:  (2,310)
INFO:    Starting work: i = 3
INFO:    Ending work: result = (3,273)
Result:  (3,273)
Warning

Be very careful about overriding concrete methods! In this case, we don’t change the behavior of the the parent-class method. We just log activity, then call the parent method, then log again. We are careful to return the result unchanged that was returned by the parent method.

To mix in traits while constructing an instance as shown, we use the with keyword. We can mix in as many as we want. Some traits might not modify existing behavior at all, and just add new useful, but independent methods.

In this example, we’re actually modifying the behavior of work, in order to inject logging, but we are not changing its “contract” with clients, that is, its external behavior.3

If we needed multiple instances of Service with StdoutLogging, we should declare a class:

class LoggedService(name: String)
  extends Service(name) with StdoutLogging:
  ...

Note how we pass the name argument to the parent class Service. To create instances, new LoggedService("three") works as you would expect it to work.

There is a lot more to discuss about traits and mixin composition, as we’ll see.

Recap and What’s Next

We’ve covered a lot of ground in these first chapters. We learned how flexible and concise Scala code can be. In this chapter, we learned some powerful constructs for defining DSLs and for manipulating data, such as for comprehensions. Finally, we learned more about enumerations and the basic capabilities of traits.

You should now be able to read quite a lot of Scala code, but there’s plenty more about the language to learn. Next we’ll begin a deeper dive into Scala features.

1 You can find a Scala 2 version of WeekDay in the code examples, src/script/scala-2/progscala3/rounding/WeekDay.scala.

2 Not to be confused with the keyword using that we discussed in ATasteOfFutures.

3 That’s not strictly true, in the sense that the extra I/O has changed the code’s interaction with the “world.”

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

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