2. Week 02: Scala Primer
This lecture is an introduction to Scala programming language. It is intentionally brief, as all the YSC4231 attendies are assumed to have familiarity with either OCaml or Java (or both). Therefore, this tutorial provides a quick overview of Scala’s object-oriented and functional features
The accompanying code for this lecture is available in the branch
01-intro
(under the folder src/main/scala/intro
) of the
course repository.
If unsure about some of the feature, you are encouraged to take a look at one of multiple Scala tutorials available online. The following are the recommended ones:
2.1. Using IntelliJ IDEA
We will be using IntelliJ IDEA as an Integrated Development Environment (IDE) for programming in Scala. It will allow us to manage large multi-file projects with multiple executables and automated tests. The following link provides instructions on getting IntelliJ and creating a simple Scala project in it:
You can also open the course repository from IntelliJ via File -> Open dialogue.
IntelliJ IDEA provides a convenient way to build projects, navigate to files and definitions, and refactor them consistently. You are encouraged to this tutorial to master the following aspects of using IntelliJ for managing Scala projects:
Creating new files
Using code completion
Building and rebuilding the project
Navigating to definitions and files (see the menu Navigate)
Creating runtim configurations
Use this key binding reference for your convenience. Alternatively, just remember how to invoke the Find Action dialogue (Ctrl + Shift + A or Command + Shit + A, depending on the system) and type in your query to get a hint.
The following simple executable is implemented in the file
SquareOf5.scala
(use IntelliJ navigation to locate it!):
object SquareOf5 extends App {
def square(x: Int) = x * x
val s = square(5)
println(s"Result $s")
}
To run it in the IDE, right-click on SquareOf5
in the editor (or
on the file in the navigator tree pane on the left of the editor) and
choose Run ‘SquareOf5’. Alternatively, you can click on the green
triangle next to the object definition. The result of executing the
program will appear in the execution pane at the bottom of the screen.
An alternative way to define an executable object
is via the
main
method. For instance, it can be done as follows:
object SquareOf6 {
def main(args: Array[String]): Unit = {
val s = SquareOf5.square(6)
println(s"Result $s")
}
}
Here, the executable object SquareOf6
uses the function square
defined within the object SquareOf5
.
2.2. Using SBT
Scala Build Tool is a build system used to manage dependencies and
compile Scala projects, similar to OCaml’s dune
and opam
. The
tutorial on obtaining and running sbt
is available:
You can try executing sbt test
from the root of the primer
project. This will compile it and automatically run all the tests in
the project.
Finally, to run a Scala console similar to OCaml utop
, start
sbt
and then type console
in it. You can now import the code
from the packages and objects of your project, as well as create new
class instances.
2.3. Object-Oriented Elements of Scala
Scala inherits many object-oriented features from languages such as C++ and Java. That said, it’s object-oriented model can still be understand in terms of OCaml abstractions. Definitions of functions and data types in Scala can be placed in either of the module-like environments:
Objects are similar to OCaml’s first-order modules. They may contain
data type definitions, functions and mutable references.
Each object is unique, hence all values in it are global. Examples
of objects are shown above. Objects are the “final” units of any
Scala project: all executable code is run starting from some
object’s main
method (or is an object is defined as extends App
)
as demonstrated above.
Classes are “factories” of objects. Similarly, they may contain
definitions of functions (called methods in Scala), as well as
data types. However, each class can be used to create multiple
instances, each having its own state (including mutable variables,
called fields). For instance, the following class features one
mutable field greeting
and three methods:
class Printer(val initial: String) {
private var greeting = initial
def printMessage(): Unit = println(greeting + "!")
def printNumber(x: Int): Unit = {
println("Number: " + x)
}
def setGreeting(s: String) = {
greeting = s
}
}
It takes a value of type String
, which can be then used within the
body of the class. For instance, it is used to initialise a mutable
field greeting
. It can be later changed via a method
setGreeting
by passing a new String
value. The class can be
then instantiated and used as follows:
object UsePrinter {
def main(args: Array[String]): Unit = {
val printer = new Printer("Hello")
printer.printMessage()
printer.printNumber(42)
}
}
Traits in Scala are used as “templates” containing some data types, fields, and methods, which can be used to “mix-in” to classes and objects that are create later. Traits cannot be instantiated or executed: they should be considered as “dictionaries” of definitions to be used by classes and objects. The following example shows a trait, a class that extends it, and an object that uses the resulting class:
trait Logging {
// Signature of a method to be defined later
def log(s: String): Unit
def warn(s: String) = log("WARN: " + s)
def error(s: String) = log("ERROR: " + s)
}
// A class that mixes in Logging's functionality
class PrintLogging extends Logging {
def log(s: String) = println(s)
}
object UseLogging extends App {
val logger = new PrintLogging
logger.warn("Hmm...")
}
One class and object can extend multiple traits. In addition, an
object and a class can extend precisely one class.
More details on object-oriented model of Scala can be found in the following tutorials:
Classes, objects and traits in Scala are located under a source root in a hierarchy of folders that correspond to packages. Packages are declared at the beginning of each file and should mimic the folder hierarchy, for instance:
package runners.squares
is a package ascribed to SquareOf5
and SquareOf6
objects that
are located in a folder runners/squares
under the source root of a
project.
Classes and traits in scala can be polymorphic, i.e., taking type
parameters. For instance, the following class corresponds to OCaml’s
polymorphic pair
data type with immutable fields first
and
second
or arbitrary types P
and Q
:
class Pair[P, Q](val first: P, val second: Q)
object UsePair extends App {
val p = new Pair("abc", 42)
println(p.first)
println(p.second)
}
2.4. Functional Elements of Scala
Scala allows to define classes in a way that they could be used in a
pattern matching, similar to OCaml’s data types. For this, they need
to be defined using the keyword case
. The typical pattern is to
define a trait that corresponds to the “abstract data type” and a
number of case classes that extend it. For instance, this is how the
familiar option type is implemented in Scala (ignore the +
in
front of the type parameter for now):
trait Option[+T]
case class Some[T](elem: T) extends Option[T]
case object None extends Option[Nothing]
Notice that None
is defined as an object, since it doesn’t take
any parameters, and hence should only exist as a single instance. This
is how case classes are used in pattern matching:
object PrintOption {
def printOption(o: Option[String]): Unit = {
o match {
case Some(e) =>
println("Some: " + e)
case None =>
println("Nothing!")
}
}
def main(args: Array[String]): Unit = {
val s = Some("Hello")
printOption(s)
}
}
Scala has support for first-class functions-as-values. The following code snippet declares a lambda-expressions for multiplying a number by two:
val twice: Int => Int = (x: Int) => x * 2
Notice that it is ascribed the type Int => Int
. However, the compiler can
infer this type automatically cased on the argument type of x
,
hence we can shorten the definition and write in one of the two
following ways:
val twice = (x: Int) => x * 2
or:
val twice: Int => Int = x => x * 2
First-class functions allow manipulating blocks of code as if they
were first-class values. For instance, the following example uses
lazy (by-name) parameter to declare a run-twice method, which runs
the specified block of code body
twice:
def runTwice(body: =>Unit): Unit = {
body
body
}
// This will print Hello twice.
runTwice ({
println("Hello!")
})
Scala provides for-comprehensions as a
convenient way to travers and transform collections. For instance, the
following loop prints all elements of a list ls
:
val ls = List(1, 2, 3, 4)
for (e <- ls) {
println(e)
}
The standard functions on collections, such as map
, foldLeft
,
filter
etc. are all available as methods of the corresponding data
types. For instance, the following code prints the list that is
obtained by multiplying all elements of ls
by 3:
println(ls.map(x => x * 3))
Notice that the parameter type of x
of the function passed to
map
is inferred from the type of map
itself and does not have
to be declared explicitly.
Scala features both immutable (purely functional) and mutable
collections. All collections mix-in the functionality from the
Seq[T]
trait of generic collections of elements of type T
.
Concrete collections can be created and manipulated as follows:
val messages : Seq[String] = Seq("Hello", "World", "!")
val messageMap : Map[Int, String] = Map(1 -> "Hello", 2 -> "World", 3 -> "!")
// Convert all elements of `messages` to strings (which they already are),
// concatenate with the " " as a separator and print the result:
println(messages.mkString(" "))
//Create a new map by adding a key-value pair (4 -> "Yay") to messageMap
println(messageMap + (4 -> "Yay"))
We recommend to use IntelliJ auto-completion to discover available methods for different data types. The following tutorial provides a great overview of Scala collections:
2.5. Imperative elements of Scala
Scala allows one to use arrays in the same way one uses them in OCaml and Java. The code below shows some examples of creating using arrays:
object ArrayExample {
var index = 0
val nextIndex = () => {
val tmp = index
index = index + 1
tmp
}
def main(args: Array[String]): Unit = {
// Create an array of booleans of size 5
val arr1 = new Array[Boolean](5)
// Print all elements of the array
for (i <- 0 to arr1.length - 1) {
println(arr1(i))
}
// Create an array of size 5 filled by repeating the computation
// passed as a second parameter
val arr2 = Array.fill(5)({ nextIndex() * nextIndex() })
// Print all elements of this array
for (i <- arr2.indices) {
println(arr2(i))
}
}
}
The following reference provides a brief guide on using arrays in conjunction with other Scala collections:
2.6. Working with Threads in Scala
Threads in Scala can be created as instances of a class that extends
Thread
, overriding its method run
. Consider the following
object that features a mutable list field and three methods to
manipulate with it:
object ConcurrentManipulation {
private var workingList: List[Int] = Nil
The first method is used to add an element to a list in a mutually-exclusive fashion:
def addToList(i: Int) = {
this.synchronized {
workingList = i :: workingList
}
}
The two other methods are removing elements from the list. The first one does it without synchronisation, and the second one enforces mutual exclusion:
def removeFromListWithoutSync = {
// Notice how synchornisation is missing
// This can cause troubles: which?
workingList match {
case Nil => None
case ::(head, tl) =>
// Wait for some time
Thread.sleep(10)
workingList = tl
Some(head)
}
}
def removeFromList = this.synchronized{
workingList match {
case Nil => None
case ::(head, tl) =>
// Wait for some time
Thread.sleep(10)
workingList = tl
Some(head)
}
}
Notice that in removeFromListWithoutSync
, to make things worse, we
added a delay of the form Thread.sleep(10)
that makes a thread
executing this method to stop for 10 milliseconds.
We can now create two threads for adding and removing elements of
workingList
:
class Adder(start: Int) extends Thread {
override def run() = {
// Get my thread id
val id = Thread.currentThread().getId
val end = start + 9
for (i <- start to end) {
addToList(i)
println(s"Thread $id: Added $i")
}
}
}
class Remover extends Thread {
override def run() = {
// Get my thread id
val id = Thread.currentThread().getId
var result: Option[Int] = None
do {
result = removeFromList
result match {
case Some(value) =>
println(s"Thread $id: Removed $value")
case None => // Do nothing
}
} while (result.isDefined) // Loop while the list is not depleted
}
}
Time to put this all together. The following method first creates two
threads to add elements to the list and then starts them, so they
could run concurrently (via the start()
method). It then waits for
them to finish their execution (via the join()
method) and starts
two more threads to remove the elements:
def main(args: Array[String]): Unit = {
// Create two threads without executing them
val adder1 = new Adder(1)
val adder2 = new Adder(11)
// Start two threads in parallel with this one
adder1.start()
adder2.start()
// Wait in this thread while those two will finish
adder1.join()
adder2.join()
println()
// Make two new threads
val remover1 = new Remover
val remover2 = new Remover
// Start two threads in parallel with this one
remover1.start()
remover2.start()
// Wait in this thread while those two will finish
remover1.join()
remover2.join()
}
Let us run an application to see the result of its concurrent execution, which should be similar to the following:
Thread 11: Added 11
Thread 10: Added 1
Thread 11: Added 12
Thread 10: Added 2
Thread 11: Added 13
Thread 11: Added 14
Thread 11: Added 15
Thread 11: Added 16
Thread 10: Added 3
Thread 11: Added 17
Thread 10: Added 4
Thread 11: Added 18
Thread 11: Added 19
Thread 11: Added 20
Thread 10: Added 5
Thread 10: Added 6
Thread 10: Added 7
Thread 10: Added 8
Thread 10: Added 9
Thread 10: Added 10
Thread 12: Removed 10
Thread 13: Removed 9
Thread 12: Removed 8
Thread 12: Removed 7
Thread 13: Removed 6
Thread 13: Removed 20
Thread 12: Removed 19
Thread 13: Removed 5
Thread 12: Removed 18
Thread 13: Removed 4
Thread 12: Removed 17
Thread 13: Removed 16
Thread 12: Removed 15
Thread 13: Removed 14
Thread 12: Removed 3
Thread 13: Removed 13
Thread 12: Removed 2
Thread 13: Removed 12
Thread 13: Removed 1
Thread 12: Removed 11
As we can see the threads are fetching the elements in the interleaved order, but the set of removed elements is the same as the set of added ones.
One, let’s try to use removeFromListWithoutSync
instead of
removeFromList
in the implementation of the Remover
thread.
Can you explain the result? Can you fix removeFromListWithoutSync
by adding the necessary synchronisation to it to avoid this problem?
As the last not, please, pay attention to the difference between
run
and start
methods of thread instances. The first one can
only be used to to define the things the thread must do (you must
not call it), while the second is used to begin the execution of a
thread concurrently with the current one.
2.7. Writing Automated Tests in Scala
Scala provides many frameworks for implementing automated tests. We
will use ScalaTest. The tests located
under the test
source root of the project and adhering to
ScalaTest’s conventions will be automatically executed by SBT and IntelliJ.
Feel free to copy the following template for your tests from the Scala primer project:
import org.scalatest.{FunSpec, Matchers}
// The test needs to extend those two traits in order to
// get access to the functions used below
class BasicTest extends FunSpec with Matchers {
// Describe a set of tests for some class of cases
describe("A simple test") {
// Describe an individual test
it("should always succeed") {
// Write your code here
// Use assert statements to make the test pass or fail
assert(2 * 2 == 4)
}
}
// Another set of tests
describe("A square function") {
it ("should work correctly") {
// Import all from object SquareOf5
import primer.runners.squares.SquareOf5._
assert(square(10) == 100)
}
}
}
You can run an individual test suite (a class) in IntelliJ by
right-clicking on its name and choosing Run ...
. The same can be
done and a finer level of granularity by clicking on the individual
describe
-component of a test suite. Finally, you can run all
tests in the project by right-clicking on the test source folder in
the project view on the left of the screen and choosing Run 'ScalaTest in scala...'
.