Scala Fundamentals (1)

1- Block expression

The intermediate results (vals/vars) in a block expression are local. Example:

val firstName = "Polar"
def name: String = {
  println("This is a block")
  val title = "Mr."
  val lastName = "Bear"
  title + " " + firstName + " " + lastName
}
println(name)
println(title)

Output:
firstName: String = Polar
name: String
This is a block
Mr. Polar Bear
Error:(9, 17) not found: value title
println(title)

Block expression are useful for evaluating intermediate expressions and assigning the result to a val/var.

2- Objects and classes

Everything is a kind of object in Scala; even primitive types, Int, Boolean, are objects. Also the program code to be run is an object and is defined in the form/pattern of an object. All objects can have methods and fields (attributes). Methods of an objects give functionality to the object to do task or computation on data. Fields are for storing data. Every object belongs to a class that have only one instance.

A class is an abstract template for creating objects that have similar methods and fields. A class also defines/create a Type (of objects) and therefore objects of the same class have the same Type. A class is not a value (like functions and objects) and it lives in its own namespace. Scala has two namespace: namespace of values, and namespace of types. Classes live in the type namespace.

The functionality of an object (if it has one) can be called like a function. A function is a first class value whereas a method belongs to an object. For example:

object addNumbers{
  def addEm (a: Int, b: Int) = println(a + b)
}
addNumbers.addEm(1,2)

// compare with

object addNumbers{
  def apply (a: Int, b: Int) = println(a + b)
}
addNumbers(1,2)

Function-like application of any object is available by using the apply method.

An executable Scala program is an object with an entry point, i.e. the main method. An object can be made executable (or an executable Scala code) by either adding a main method, or making the object extends the type App:

object MyProgram{  // this is a singleton objec
  import ... //import libraries
   def main(args : Array[String]) : Unit = {
     // Array[String] are used for the command line arguments
     // body of the main program to do stuff
   }
}

// or

object MyProgram extends App {
   // body of the main program to do stuff
}

1- Singleton object

Is an objects which is unique and we cannot create other values of this object. Singleton object belongs to a class that have only one instance. As it can be seen the type of a singleton object defined as bellow is itself. The term Box$@3ae78a4c indicated the type @ a unique (reference) identifier of the object. All types of objects have unique identifiers.

object Box
res0: Box.type = Box$@3ae78a4c

2- Object (normal)

An object should belong to a predefined class. Example:

class Animal(val kind: String, val location: String) {
  def loc = println(kind + " lives in " + location)
}

val polarbear_1 = new Animal("Bear", "north pole")
polarbear_1.loc
polarbear_1.kind

// results
defined class Animal
polarbear_1: Animal = Animal@669cb6c7

Bear lives in north pole
res2: String = Bear

The class Animal creates a new type Animal that can be used like any other types. This is a type of an object.

3- Companion Objects

A companion object of a class is a singleton object with the same name as a class (and defined in the same file as the class’s file). The class is also called the companion class of the object. These two live in different namespaces, i.e. value and type namespaces. It is then important to note that the companion object is not an instant of its companion class. One application is to have the (singleton) companion object manage and implement the constructors of an instant (object) of the companion class. In this case the companion object is an auxiliary tool. For example:

class Line(val point_1:Double, val point_2:Double){
  def printPoints = println(point_1,point_2)
}
val line_1 = new Line(0.0, 1.0) // Line is the name of the class
line_1.printPoints

// Compare with

class Line(val point_1:Double, val point_2:Double){
  def printPoints = println(point_1,point_2)
}

object Line{
  def apply(point_1:Double, point_2:Double): Line = new Line(point_1,point_2)
}

val line_1 = Line(0.0,1.0)  // Line is the name of the object 
line_1.printPoints

4- Case classes

Case classes are useful shorthand for defining a class, its companion objects and several useful features at once. They are ideal for lightweight data-holding classes. Defining a case class is as of a class, however, with adding the literal case, and the val keyword for the constructors is optional. It also does not need a new keyword as there is a companion object with an apply method for the purpose of creating the case class. Example:

case class Animal (kind:String, location:String){
  def loc = println(kind + " lives in " + location)
}

// instantiate a case class using its class
val polarbear_1 = new Animal("polar bear", "north pole")
polarbear_1.loc

// instantiate a case class using its companion object automatically built
val polarbear_2 = Animal("polar bear", "north pole")
polarbear_2.loc

Features of case classes

1- Sensible equal and hashCode methods acting on the field values of the objects rather than the reference identity. Example:

polarbear_1 == polarbear_2
// result
res2: Boolean = true // but two different values

polarbear_1.eq(polarbear_2)  //eq method compares the reference ids
//result
res2: Boolean = false

2- Copy method

val polarbear_3 = polarbear_1.copy()  // it is a shallow copy

3- Pattern matching using case classes. Pattern match is an extended if.

To match against the constructor values. Example:

case class Animal (kind:String, location:String){
}

object inspectAnimal{
  def inspect(anAnimal: Animal): String =
    anAnimal match {  // it reads: check if anAnimal matches a case and then output as insructed
      case Animal("bear", "north pole") => "this is a polar bear"
      case Animal("bear", "Banff") => "this is a Grizly or black bear"
      case Animal(_, "Banff") => "this might be a bear" 
      case Animal(its_kind,its_location) => s"this is a/an $its_kind living in $its_location"
		//binds its_kind, its_location to the constructor values    
    }
  
}

val animal_1 = Animal("bear", "north pole")
val animal_2 = Animal("Marmot", "Banff")
val animal_3 = Animal("cat", "my house")

inspectAnimal.inspect(animal_1) // result: this is a polar bear
inspectAnimal.inspect(animal_2) // result: this might be a bear
inspectAnimal.inspect(animal_3) // result: this is a/an cat living in my house

To mach against the type. Example: https://docs.scala-lang.org/tour/pattern-matching.html.

5- Abstract class

3- Tips with objects and classes

1- Overloading the methods (overloaded methods)

Instead of creating several methods with different names and doing the same task but receiving different number of arguments and/or different types of parameters, we can create methods with the same name but different parameter and let Scala decide on the proper version of the method. Overloading methods is applicable to classes and singleton objects. Example:

object doSomething{
  def sumNumbers(a:Int, b:Int): Unit = println(a + b)
  def sumNumbers(a:Double, b:Double): Unit = println(a + b)
  def sumNumbers(a:Int, b:Int, c:Int): Unit = println(a + b + c)
}

doSomething.sumNumbers(2,3) // 5
doSomething.sumNumbers(12.345,34.567) // 46.912
doSomething.sumNumbers(1,2,7) // 10

2- Auxiliary constructor

Other than the primary constructors of a class, auxiliary constructors can also be utilized. Auxiliary constructors overload the constructors. To do this, the following should be considered:

  • Auxiliary constructors are defined by def keyword.
  • Like Method Overloading, all auxiliary constructors should use the same name this.
  • each auxiliary constructor must have a different different parameters list.
  • each auxiliary constructor must first call a previously defined primary constructor(s) or/and an auxiliary constructor(s). This is performed by name this.

Nevertheless, constructor overloading can be implemented by setting default values for the primary constructors.

4- Traits (Subtyping)

Classes are abstraction over objects while traits are abstraction over classes; in other words, traits are template for classes. Traits allow us to define a super type for several classes which are the subtypes. This super type is is outside of the Any super type that all classes share. Classes under a trait can share the implementation of the same operation. A trait creates interface that any subtype, i.e. classes, must implement (comply with).

Compared with a class, a trait does not/cannot logically have a constructor. Classes (having constructors) can be created from a trait and objects from the classes. A trait can define abstract fields and methods that have names and type signature but no (concrete) implementation. An implementation is possible when defining an extending class under the trait. Implementing methods based on abstract methods in a trait is possible. Whatever abstract methods and fields are defined in a trait have to be then implemented in the extending class(es). Concrete (default) implementation of any method (or a field) in a trait is possible as well, however, it must be make sure that the method is valid in all the extending classes (subtypes). Otherwise, the method (or field) should be overridden in a subtype.

Abstract fields in a trait can be defined either using val or def. However, it is recommended to use def because def is a generalization of val in Scala. If an abstract field is defined by def, then val will be used for its implementation in the extending class.

Abstract methods (or fields) can have completely different implementations in different subtypes.

Example: Animals can be wild or pet. Instead of creating two separate non-connected (in terms of sharing fields and methods) classes, we create two extending classes under a trait.

trait Animal {
  def kind: String  // can also be val kind: String
  def location: String
  def inspect: Unit = println(s"$kind lives in $location")
  def id: Unit
}

case class Wild (kind: String, location: String, idNumber: Int) extends Animal {
  def id: Unit = print(s"The wild animal id is: $idNumber")
}

case class Pet (kind: String, location: String, idNumber: Int) extends Animal{
  def id: Unit = print(s"The pet id is: $idNumber")
  
}

val bear_1 = Wild("bear", "north pole", 1212123)
val cat_1 = Pet ("Cat", "my house", 123456)
bear_1.inspect
bear_1.id
cat_1.inspect
cat_1.id
//results
/*
defined trait Animal

defined class Wild

defined class Pet

bear_1: Wild = Wild(bear,north pole,1212123)

cat_1: Pet = Pet(Cat,my house,123456)

bear lives in north pole
The wild animal id is: 1212123
Cat lives in my house
The pet id is: 123456
*/

Scala with IntelliJ IDEA

1- Defining a package object

  1. On the project side bar right click on Scala folder –> new –> package.
  2. name the package custom.methods.
  3. Right click on custom.methods –>new –> package object

The package object is now ready.

package custom
package object methods {
// define your custom classes, case classes, objects, methods here
}