Scala Coding Conventions: An Introduction

Picking up a new programming language can be tricky, this guide aims to simplify that for those looking to learn Scala. We will introduce and go over the main ideas one should know to program in Scala, including expressions, data types, as well as abstraction of data and control.


Introduction to Expressions

Expressions, Values, and Variables

Simply put, Expressions are computable statements, such as adding two values or string concatenation, and a Value is the result of an expression. Using the val keyword, The following Scala code stores a computed value and prints it.

val x = 1 + 1 // implicit declaration
println(x)
val y: Int = 1 + 1 // explicit declaration

Here, the value type of x is inferred, but you can also explicitly declare the value type, as it was for y. Note the necessary : after the value name and before the type name.

Note that values CANNOT be re-assigned, meaning they are constant. If re-assigning is needed, then one should instead declare a variable with the keyword var. You can choose to explicitly state the type of variable or not, just like values.

var x: Int = 1 + 1
x = 3 // will compile

Finally, brackets {} can be used to combine expressions into blocks. Note that in a code block, the expression in the block is the returned value.

println({
val x = 1 + 1
x + 1
}) // prints 3

Functions and Methods

Functions are expressions that take parameters. In Scala, they can be declared either anonymously or be given a name.

(x: Int) => x + 2  // anonymous
val addTwo = (x: Int) => x + 2 // named

Observe the syntax: The => operator here creates a function instance. To the left of the => are the function parameters, and to the right is the involved expression. Functions can have many parameters or none at all. Functions can also be a part of another function’s expression.

val add = (x: Int, y: Int) => x + y
println(add(1, 2)) // 3

val getAdd = () => add(3, 4)
println(getAdd()) // 7

Methods are similar to functions, with some minor differences. Firstly, they must be defined with the def keyword. Secondly, methods can have multiple parameter lists (or none at all). Finally, a return type must be specified after the parameter list(s), denoted by a : followed by the return type and value.

def addThenMultiply(x: Int, y: Int)(multiplier: Int): Int = {
(x + y) * multiplier
}
println(addThenMultiply(1, 2)(3)) // 9

Multiple Parameter Lists and Currying

Currying is a technique used in Scala that transforms a function with multiple arguments into a function with only one argument. This single argument is the first value of the original function. The function then returns another single argument function. This function then has access to the first value that was returned and has a function that takes a single argument, which would be the second value of the original function. This chaining would continue on for as many arguments as the original function had. The last function in the chain would have access to all to the arguments from earlier in the chain and could do whatever was necessary of the original function.

In Scala, there is a shorthand way of currying and the standard way of currying. The shorthand for currying is just separating the arguments. The long way of currying involves a slightly different approach.

def multiply(multiplier1: Int): (Int => Int) = {
(multiplier2: Int) => {
multiplier1 * multiplier2
}
}
// store multiply function, where multiplier1 = 3
val multiplyBy = multiply(3)
// call stored function, finish calculation with multiplier2 = 4
val resultMultipliedBy4 = multiplyBy(4) // = 12

This is an example of the longhand version of currying. The following is a shorthand version of the same code that allows you to list one argument after the other. Note the (_) when we initially store the multiply function.

def multiply(multiplier1: Int)(multiplier2: Int): Int = {
multiplier1 * multiplier2
}
val multiplyBy = multiply(3)(_)
val resultMultipliedBy = multiplyBy(4) // 12

Data Types

All values in Scala have a data type, functions included. The following diagram from the official documentation illustrates the data type hierarchy of the Scala language.

Source: https://docs.scala-lang.org/tour/unified-types.html

Here are some of the most important things to take away from this diagram:

  • The Any type is the supertype of all types, and has two direct subclasses AnyVal and AnyRef.
  • The AnyVal type represents value types, which includes Double, Float, Long, Int, Short, Byte, Unit, Boolean, and Char. while AnyRef represents reference types.
  • Unit is a type that carries no meaningful information. It is similar to the void type from the C languages.
  • The AnyRef type represents reference types, and includes all user-defined types.
  • Nothing is the subtype of all types, and does not evaluate to a value. Common use of the Nothing type is to signal non-termination in code.
  • Null is the subtype of all reference types, and is invoked with the keyword null.

Type Casting

The following diagram represents Scala’s type casting rules. If type A points to type B, then type A can be cast into type B. However, the inverse is NOT true.

Source: https://docs.scala-lang.org/tour/unified-types.html

Here is a simple example that illustrates this typecasting hierarchy.

var x: Int = 50000
var y: Long = x // will compile, Int cast into Long
var z: Short = x // will NOT compile, Int cast into Short

The same logic applies when performing arithmetic on two numbers of different types. Scala will make the resulting variable of the ‘upper’ type. For example, adding an Integer and a Short must result in an integer. Multiplying a Short and a Long results in a Long, and so forth.

Even though casting is one-way, Scala has various functions that allows us to cast backwards. However this should always be done carefully as to avoid type mismatching, loss in precision, or overflow and underflow. Here are a couple of examples:

var myInt: Int = 100000
var myShort: Short = 5
println(myInt) // 100000
println(myInt.toShort) // -31072, overflow occurs

var result1: Int = myShort + myInt // will compile, 100005
var result2: Short = myShort + myInt.toShort // ERROR, Int expected

Abstraction in Scala

In general, there are two main kinds of abstraction in computer science; Data Abstraction and Control Abstraction. These concepts are used in programming practices to improve program efficiency and readability.


Data Abstraction

In general, Data Abstraction refers to hiding details of data, leaving only the essential details to make the data easier to work with and comprehend. We now go over some structures in Scala related to this idea of abstraction.

Classes

Classes are defined with the class keyword, followed by its name and its construction parameters. You can then create an instance of a defined class with the new keyword.

class Student(name: String, age: Int) {
def summary(major: String): Unit =
println(name + ", " + age.toString + ", " + major)
}

val testStudent = new Student("John Doe", 21)
testStudent.summary("CompSci") // John Doe, 21, CompSci

Scala also has Case Classes. Unlike ordinary classes, case classes by default are immutable and compared by value. Case classes also do not require the new keyword for instantiation.

case class Name(first: String, last: String)

val personA = Name("John","Doe")
val personB = Name("John","Doe")
val personC = Name("Susan","Blanco")

val compareNames = (x: Name, y: Name) => x == y
compareNames(personA, personB) // true
compareNames(personA, personC) // false

Objects

Objects are defined with the object keyword. Objects are essentially singleton classes, and can be accessed by their name.

object InstanceTest {
private var counter = 0
def addOne(): Int = {
counter += 1
counter
}
}

val firstInst: Int = InstanceTest.addOne()
val secondInst: Int = InstanceTest.addOne()

println(firstInst) // 1
println(secondInst) // 2

Abstract Types: Traits and Abstract Classes

Traits are used to share interfaces and fields between classes. They have no construction parameters and cannot be instantiated. Instead, traits are inherited by classes and objects using the extends keyword. You can also implement a trait’s abstract members by with the override keyword.

trait printer {
def echo() {
println("Testing echo method.")
}
}

class printClass(message: String) extends printer {
override def echo() {
println(message)
}
}

var test = new printClass("Hello World!")
test.echo() // prints "Hello World"

Abstract Classes are similar to traits, with some minor differences. Firstly, Abstract Classes can have construction parameters and type parameters, while traits can only have type parameters. Secondly, a class can inherit many traits, but only one abstract class. Scala class abstraction mechanism is very similar to the Java abstraction mechanism. A class in Scala can extend only one abstract class and does not support multi-inheritance. A class can have abstract as well as non-abstract methods in Scala.

Abstract methods are methods which do not have any body. In other words, they do not contain an implementation.

// Abstract class
abstract class author {
// abstract method
def details()
}
// Book class extends abstract class
class Book extends author {
def details() {
println("Author name:Joe")
println("Topic name: Abstract class in Scala")
}
}
object Main {
// Main method
def main(args: Array[String]) {
// objects of Book class
var obj = new Book()
obj.details()
}
}

In Scala, we cannot instantiate objects of an abstract class, otherwise the compiler will give an error. These abstract classes also contains fields. These fields can be accessed by the methods of abstract class or the methods of classes implementing the abstract class. Like Java, Scala abstract classes also have constructors which are called when an instance of the class that inherits the abstract class is created. An abstract class in Scala can contain final methods to avoid being overridden.

Variances

From the official Scala Documentation, variance is defined as follows:

“Variance is the correlation of subtyping relationships of complex types and the subtyping relationships of their component types.”

In Scala, variance is applied to type parameters of generic classes to help clarify and refine the relationships between classes. There are three types of variance: Covariance, Contravariance, and Invariance.

A covariant class consists of a type parameter A which can be denoted as +A. This implies that for a covariant class List[+A], two type parameters A and B where A is a subtype of B then, List[A] is a subtype of List[B].

In the above example, List[Cat] is a List[Animal] and List[Dog] is a List[Animal] because of the covariant parameter. Because of that, printAnimalNames(cats) and printAnimalNames(dogs) are compiled without error.

A contravariant class can be represented as some class with a parameter type A denoted as -A. That is, for some class Writer[-A], the type parameters A and B where A is a subtype of B then Writer[B] is subtype of Writer[A]. This parameter and class relationship is the opposite of that in covariance, and also opposite with respect to the subtyping relationship.

In the above example, the output of both printMyCat(catPrinter) and printMyCat(animalPrinter) will be same — Boots. This is because a printer[Animal] can print any animal and hence can print any cat. The inverse, however, is not true.

The classes in Scala are invariant by default, meaning these classes do not have covariance or contravariance properties.

In the above example, a Container[Cat] is not a type of Container[Animal] and vice-versa. It may seem like a covariant class but making this mutable generic class a covariant would not be safe.

Inheritance in Scala

Scala supports many different types of inheritance. We will go over each kind using simple examples.

First is single inheritance. Suppose we have the following two classes, Grandparent and Parent. By making Parent inherit from Grandparent, we demonstrate single inheritance. For example:

class Grandparent {
var FirstName: String = "Julius"
var SecondName: String = "Caesar"
}

class Parent extends Grandparent {
var Age: Int = 40
}

Recall that the extends keyword implements inheritance. Using the code above, we can create a new Parent object and call the either the FirstName or SecondName variable inherited from the Grandparent class.

var single = new Parent()
println("Name: " + single.FirstName) // "Julius"
println("Age: " + single.Age.toString) // "40"

Continuing from our previous example, we can have multilevel inheritance if we create another class that inherits the Parent class. For example, we create the Child1 class:

class Child1 extends Parent { 
var FavoriteFood: String = "Pizza"

def details() {
println("Name: " + FirstName)
println("Age: " + Age.toString)
println("Favorite Food: " + FavoriteFood)
}
}

var multilevel = new Child1()
multilevel.details() // Julius, 40, Pizza

Now the class hierarchy is as follows: Child1 inherits Parent inherits Grandparent.

Hierarchical Inheritance is when one class has multiple subclasses. Adding even further to our example, we can create a Child2 class that also inherits from Parent. At this point, the Parent class now acts as the base class for both the Child1 and Child2 classes; a form of hierarchical inheritance.

class Child2 extends Parent { 
var FavoriteFood: String = "Ice Cream"

def details() {
println("Name: " + SecondName)
println("Age: " + Age.toString)
println("Favorite Food: " + FavoriteFood)
}
}

var hierarchical = new Child2()
hierarchical.details() // Caesar, 40, Ice Cream

Multiple inheritance is the opposite of hierarchical influence; that is, when one class inherits from multiple base classes. However, recall that Scala does not allow a class to inherit from more than one base class. Instead we must inherit multiple traits, which Scala does allow. Note this example is different from the previous ones.

trait First { def FirstMethod() } 
trait Second { def SecondMethod() }
trait Third { def ThirdMethod() }

class Multiple extends First with Second with Third {

def FirstMethod() { println("First Trait") }

def SecondMethod() { println("Second Trait") }

def ThirdMethod() { println("Third Trait") }
}

var multi = new Multiple()
multi.FirstMethod() // "First Trait"
multi.SecondMethod() // "Second Trait"
multi.ThirdMethod() // "Third Trait"

Here we create three traits First, Second, and Third. and a class Multiple that inherits all three traits. To extend more than one trait, we use the with keyword after extending the first trait. In Scala, traits that compose a class are known as mixins.

Finally, Hybrid Inheritance is a combination of two or more of the previously listed types of inheritance.


Control Abstraction

Control Abstraction means to hide complex execution flows of a program behind a simplified representation. Such examples are for loops and while loops found in C and Java languages.

If-Else Statements

If-Else statements work the same way in Scala as most other languages. There is a boolean statement in the if section. “If” this statement is true, it expression in the if portion is executed. “Else” the statement is false, it executes the else section. Else and else if sections are optional. Else if sections are after the initial if, and before a final else.

def ifTest(x: int) {
if(x < 10) {
println("This is the if")
} else if(x > 10 & x < 15) {
println("This is the else if")
} else {
println("This is the else")
}
}

There are three main rules to keep in mind when constructing these statements:

  1. If-Else statements can have zero or one else, and must come after any else if’s.
  2. If-Else statements can have zero to many else if’s, and must come before the else (if any).
  3. Once an if or else if succeeds, no other else if’s or else that follow are tested.

While Loops

While loops are loops that will continue to be executed until a condition is met and the loop is terminated. It works the same way in Scala as it does in many other languages.

var x = 0
while(x < 10) {
println("This is in the while loop”)
x = x + 1
} // prints 10 times

Match-Case Expressions

Match expressions in Scala function similarly to the switch-case statements found in C++ and Java languages. The following example from the Scala Cookbook by Alvin Alexander shows just how powerful match-case statements can be.

def echoWhatYouGaveMe(x: Any): String = x match {

// constant patterns
case 0 => "zero"
case true => "true"
case "hello" => "you said 'hello'"
case Nil => "an empty List"

// sequence patterns
case List(0, _, _) => "a three-element list with 0 as the first element"
case List(1, _*) => "a list beginning with 1, having any number of elements"
case Vector(1, _*) => "a vector starting with 1, having any number of elements"

// tuples
case (a, b) => s"got $a and $b"
case (a, b, c) => s"got $a, $b, and $c"

// constructor patterns
case Person(first, "Alexander") => s"found an Alexander, first name = $first"
case Dog("Suka") => "found a dog named Suka"

// typed patterns
case s: String => s"you gave me this string: $s"
case i: Int => s"thanks for the int: $i"
case f: Float => s"thanks for the float: $f"
case a: Array[Int] => s"an array of int: ${a.mkString(",")}"
case as: Array[String] => s"an array of strings: ${as.mkString(",")}"
case d: Dog => s"dog: ${d.name}"
case list: List[_] => s"thanks for the List: $list"
case m: Map[_, _] => m.toString

// the default wildcard pattern
case _ => "Unknown"
}

Observe that Match-case statements can match constant patterns, sequence patterns, case classes, and even typed patterns.

Foreach Loops

In Scala, the foreach method takes in a procedure (a function with result type Unit), and applies it to each element of a Scala List.

var x: List[Int] = List(1, 2, 3)
x.foreach(println)

For Comprehension

Unlike the foreach method, for comprehensions, or for-loops, are not restricted to lists. You can also refine the results of a for comprehension with an if conditional.

val names = Vector("John", "Bob", "Susan", "Billy")
for (x <- names) println(x) // John, Bob, Susan, Billy
for (y <- names if y.startsWith("B")) println(y) // Bob, Billy

Note that in this example, x <- names and y <- names are generators, where x and y here can be named to your liking. For comprehensions can have multiple generators, such as in the following example:

for (i <- 1 to 2; j <- 1 until 3 if i == j) {
println(s"i = $i, j = $j")
} // prints (i = 1, j = 1) and (i = 2, j = 2)

Note that 1 to 2 and 1 until 3 represent the same range; using the until keyword omits the upper bound. You can also traverse a range of values in multiples with the by keyword. For example, 1 to 10 by 2 would traverse through values 1, 3, 5, 7, and 9.

Map and Flatmap

In Scala, the Map method is used when you want to apply a function to a Scala List. Unlike the foreach method, map actually returns a value. We can either define our function either outside or inside of the map method.

var x = List(1, 2, 3)
def numToString (num: Int): String = {
num.toString
}

var ans1 = x.map(numToString) // List[String] = {"1","2","3"}
var ans2 = x.map(y => y.toString) // same as above
ans1.foreach(println) // We can iterate over our new Lists

flatMap is best explained by example. We will use the following method that takes an integer as input, and returns a List of two integers consisting of the original value and the other incremented by 5. We compare the differences in the values returned by the Map and Flatmap methods.

Notice that map returns a list of lists; one list for each element in numList. On the other hand, flatMap essentially flattened all of the resulting lists into one, giant list. Knowing this difference could prove beneficial if you prefer working with one List structure over the other.

Filter, filterNot, and withFilter

Filter is a method used to filter items in a collection to create a new collection that contains only the elements that match your filter criteria. The filterNot method also creates a new filtered collection from an original, but the result contains the elements that do NOT match your filter criteria.

val x = List.range(1, 10) // x = List(1, 2, 3, 4, 5, 6, 7, 8, 9)
val evens = x.filter(_ % 2 == 0) // evens = List(2, 4, 6, 8)
val odds = x.filterNot(_ % 2 == 0) //odds = List(1, 3, 5, 7, 9)

In our example, the filter method includes only the multiples of two. On the other hand, the filterNot method excludes the filtered elements, such that it does NOT include multiples of two.

withFilter performs the same evaluations that filter does to create a filtered collection but with one major difference: withFilter restricts the domain of subsequent map, flatmap, foreach, and withFilter operations. In other words, it performs ‘lazy’ filtering, whereas filter performs ‘eager’ filtering. This is more of an performance optimization than anything, but there are some cases where withFilter can lead to different output than filter. See this example for more info.

Authors

Alessandro Heres — Github
Tristen Sprainis — Github
Ayushi Priyadarshi — Github | LinkedIn