In this section, we present a short overview of the Scala programming language features that are used in the examples in this book. This is a quick and cursory glance through the basics of Scala. Note that this section is not meant to be a complete introduction to Scala. This is to remind you about some of the language's features, and contrast them with similar languages that might be familiar to you. If you would like to learn more about Scala, refer to some of the books referred to in the Summary of this chapter.
A Printer class, which takes a greeting parameter and has two methods named printMessage and printNumber, is declared as follows:
class Printer(val greeting: String) {
def printMessage(): Unit = println(greeting + "!")
def printNumber(x: Int): Unit = {
println("Number: " + x)
}
}
In the preceding code, the printMessage method does not take any arguments and contains a single println statement. The printNumber method takes a single argument x of the Int type. Neither method returns a value, which is denoted by the Unit type.
We instantiate the class and call its methods as follows:
val printy = new Printer("Hi")
printy.printMessage()
printy.printNumber(5)
Scala allows the declaration of singleton objects. This is like declaring a class and instantiating its single instance at the same time. We saw the SquareOf5 singleton object earlier, which was used to declare a simple Scala program. The following singleton object, named Test, declares a single Pi field and initializes it with the value 3.14:
object Test {
val Pi = 3.14
}
While classes in similar languages extend entities that are called interfaces, Scala classes can extend traits. Scala's traits allow declaring both concrete fields and method implementations. In the following example, we declare the Logging trait, which outputs a custom error and warning messages using the abstract log method, and then mix the trait into the PrintLogging class:
trait Logging {
def log(s: String): Unit
def warn(s: String) = log("WARN: " + s)
def error(s: String) = log("ERROR: " + s)
}
class PrintLogging extends Logging {
def log(s: String) = println(s)
}
Classes can have type parameters. The following generic Pair class takes two type parameters, P and Q, which determines the types of its arguments, named first and second:
class Pair[P, Q](val first: P, val second: Q)
Scala has support for first-class function objects, also called lambdas. In the following code snippet, we declare a twice lambda, which multiplies its argument by two:
val twice: Int => Int = (x: Int) => x * 2
Tip
Downloading the example code:
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com/support and register to have the files e-mailed directly to you.
In the preceding code, the (x: Int) part is the argument to the lambda, and x * 2 is its body. The => symbol must be placed between the arguments and the body of the lambda. The same => symbol is also used to express the type of the lambda, which is Int => Int, pronounced as Int to Int. In the preceding example, we can omit the type annotation Int => Int, and the compiler will infer the type of the twice lambda automatically, as shown in the following code:
val twice = (x: Int) => x * 2
Alternatively, we can omit the type annotation in the lambda declaration and arrive at a more convenient syntax, as follows:
val twice: Int => Int = x => x * 2
Finally, whenever the argument to the lambda appears only once in the body of the lambda, Scala allows a more convenient syntax, as follows:
val twice: Int => Int = _ * 2
First-class functions allow manipulating blocks of code as if they were first-class values. They allow a more lightweight and concise syntax. In the following example, we use byname parameters to declare a runTwice method, which runs the specified block of code body twice:
def runTwice(body: =>Unit) = {
body
body
}
A byname parameter is formed by putting the => annotation before the type. Whenever the runTwice method references the body argument, the expression is re-evaluated, as shown in the following snippet:
runTwice { / this will print Hello twice
println("Hello")
}
Scala for expressions are a convenient way to traverse and transform collections. The following for loop prints the numbers in the range from 0 until 10; where 10 is not included in the range:
for (i <- 0 until 10) println(i)
In the preceding code, the range is created with the expression 0 until 10; this is equivalent to the expression 0.until(10), which calls the method until on the value 0. In Scala, the dot notation can sometimes be dropped when invoking methods on objects.
Every for loop is equivalent to a foreach call. The preceding for loop is translated by the Scala compiler to the following expression:
(0 until 10).foreach(i => println(i))
For-comprehensions are used to transform data. The following for-comprehension transforms all the numbers from 0 until 10 by multiplying them by -1:
val negatives = for (i <- 0 until 10) yield -i
The negatives value contains negative numbers from 0 until -10. This for-comprehension is equivalent to the following map call:
val negatives = (0 until 10).map(i => -1 * i)
It is also possible to transform data from multiple inputs. The following for-comprehension creates all pairs of integers between 0 and 4:
val pairs = for (x <- 0 until 4; y <- 0 until 4) yield (x, y)
The preceding for-comprehension is equivalent to the following expression:
val pairs = (0 until 4).flatMap(x => (0 until 4).map(y => (x, y)))
We can nest an arbitrary number of generator expressions in a for-comprehension. The Scala compiler will transform them into a sequence of nested flatMap calls, followed by a map call at the deepest level.
Commonly used Scala collections include sequences, denoted by the Seq[T] type; maps, denoted by the Map[K, V] type; and sets, denoted by the Set[T] type. In the following code, we create a sequence of strings:
val messages: Seq[String] = Seq("Hello", "World.", "!")
Throughout this book, we rely heavily on the string interpolation feature. Normally, Scala strings are formed with double quotation marks. Interpolated strings are preceded with an s character, and can contain $ symbols with arbitrary identifiers resolved from the enclosing scope, as shown in the following example:
val magic = 7
val myMagicNumber = s"My magic number is $magic"
Pattern matching is another important Scala feature. For readers with Java, C#, or C background, a good way to describe it is to say that Scala's match statement is like the switch statement on steroids. The match statement can decompose arbitrary datatypes and allows you to express different cases in the program concisely.
In the following example, we declare a Map collection, named successors, used to map integers to their immediate successors. We then call the get method to obtain the successor of the number 5. The get method returns an object with the Option[Int] type, which may be implemented either with the Some class, indicating that the number 5 exists in the map, or the None class, indicating that the number 5 is not a key in the map. Pattern matching on the Option object allows proceeding casewise, as shown in the following code snippet:
val successors = Map(1 -> 2, 2 -> 3, 3 -> 4)
successors.get(5) match {
case Some(n) => println(s"Successor is: $n")
case None => println("Could not find successor.")
}
In Scala, most operators can be overloaded. Operator overloading is no different from declaring a method. In the following code snippet, we declare a Position class with a + operator:
class Position(val x: Int, val y: Int) {
def +(that: Position) = new Position(x + that.x, y + that.y)
}
Finally, Scala allows defining package objects to store top-level method and value definitions for a given package. In the following code snippet, we declare the package object for the org.learningconcurrency package. We implement the top level log method, which outputs a given string and the current thread name:
package org
package object learningconcurrency {
def log(msg: String): Unit =
println(s"${Thread.currentThread.getName}: $msg")
}
We will use the log method in the examples throughout this book to trace how concurrent programs are executed.
This concludes our quick overview of important Scala features. If you would like to obtain a deeper knowledge about any of these language constructs, we suggest that you check out one of the introductory books on sequential programming in Scala.