Small steps to Scala (5/20) - classes

Scala
5/20

Classes

  • simple class and parameterless methods
  • getters & setters
  • auxiliary constructors
  • primary constructor
  • nested classes
  • case classes
  • Scala's type hierarchy

Simple class

Scala classes are by default public so there is no need to explicitly declare that.

scala> :paste
// Entering paste mode (ctrl-D to finish)

class Counter {
    private var value = 0
    def increment() = { value += 1}
    def current = value
}

// Exiting paste mode, now interpreting.

defined class Counter

scala> val test = new Counter()
test: Counter = Counter@a38c7fe

scala> test
res0: Counter = Counter@a38c7fe

scala> test.current
res1: Int = 0

scala> test.increment()

scala> test.current
res3: Int = 1
 
scala> test.increment

scala> test.current
res4: Int = 2

Note on the aesthetics of using () parenthesis when calling a method.

It is generally considered good style to use () for a mutator method, meaning a method that changes the state of the object. In the example above, that method is increment(). However, since the method does not take any parameters, increment notation is equally valid.

For an accessor method, in case the method is defined without () parenthesis, you can only call it without (). Adding () to the method call, it will result in error: Int does not take parameters for this particular case.

If the method is defined with parenthesis (), it is up to you to include or omit it.

Below is the nice style of using the parenthesis.

test.current
test.increment()

Getters & Setters

Rather than making the properties public, as a good programming style, it is better to have methods accessing them. Here is where getters and setters come into play. Scala creates getters and setters automagically with the creation of the class.

class Test {
    var value = 10
}

In this case, Scala created automatically the setter value_ and the getter value.

val t = new Test()
t.value_=(20)    // the setter
t.value          // the getter

The above sample code will return 20.

Just because Scala is creating these two methods automatically for you, this doesn't mean you cannot modify it at your own will. Let's assume that the property value cannot go below a certain value, say 10. In that case, the setter should be modified to accommodate that constraint. The definition of the class above will become:

class Test {
    private var privateValue = 10
    
    def value = privateValue         // this is the getter
    def value_(newValue: Int) = {    // this is the setter
        if(newValue >= 10) privateValue = newValue
        else println("Value cannot get smaller than 10")
    }
}

And the results

val t = new Test()
scala> t.value
res18: Int = 10

scala> t.value_(11)

scala> t.value
res20: Int = 11

scala> t.value_(9)
Value cannot get smaller than 10

Auxiliary constructors

Scala has one primary constructor and multiple auxiliary constructors (all named this), as shown below.
Each auxiliary constructor must call either the previous constructor or the primary constructor.

class Person {
    private var name: String = "Anon"
    private var age: Int = 0
    
    def this(name: String) {    // An auxiliary constructor
        this()                  // Calls primary constructor
        this.name = name
    }
    
    def this(name: String, age: Int) {  // Another auxiliary constructor
        this(name)                      // Calls previous auxiliary constructor
        this.age = age
    }
    
    def getInfo = println(name, age)
}

Let's see what happens when we instantiate the class Person with different arguments.

No arguments

> val p1 = new Person()
> p1.getInfo
(Anon,0)

One argument

> val p2 = new Person("Alice")
> p2.getInfo
(Alice, 0)

Two arguments

> val p3 = new Person("Bob", 22)
> p3.getInfo
(Bob, 22)

Below we have another example with default arguments.

class Test(var x: Int, var y: Int = 5) {
    def addSelf(v: Test) = {
       v.x += v.x
       v.y += v.y
    }
    
    def getVal = println(x, y)
}

And the results when instantiating the class with one argument:

> val t = new Test(3)
> t.getVal
(3,5)

> t.addSelf(t)
> t.addSelf(t)

> t.getVal
(12,20)

With two arguments:

> val u = new Test(4, 9)

> u.getVal
(4,9)

> u.addSelf(u)

> u.getVal
(8,18)

Primary constructor

Every class in Scala has a primary constructor and it is automatically defined when the class is formed. At the same time, if we prefix the constructor's parameters with the val keyword, Scala will create the fields for that constructor too. Also, all statements in the class are executed by the primary constructor.

class Person (val name: String, val age: Int = 25) {
    println("Hello there! I am being executed by the primary constructor")
    
    def description = { println("## Name: " + name + "\n## Age: " + age)}
}

So, what happens when we instantiate a new Person class.

> val a = new Person("test")
Hello there! I am being executed by the primary constructor
a: Person = Person@2e43fc06

> a.description
## Name: test
## Age: 25

Nested classes

In Scala you can nest anything almost inside anything. The following example is taken from the Scala tutorials.

class Graph {
  class Node {
    var connectedNodes: List[Node] = Nil
    def connectTo(node: Node) {
      if (connectedNodes.find(node.equals).isEmpty) {
        connectedNodes = node :: connectedNodes
      }
    }
  }
  var nodes: List[Node] = Nil
  def newNode: Node = {
    val res = new Node
    nodes = res :: nodes
    res
  }
}

Case classes

Case classes are a short way of defining a class, a companion object, a field for each constructor argument, and default toString and copy methods.

case class Car(maker: String, year: Int, color: String)

object Car {
    def isFast(car: Car): Boolean = {
        car.maker match {
            case "nissan" => true
            case _ => false
        }
    }
}

The above code will run like this:

scala> Car.isFast(Car("toyota", 2018, "blue"))
res1: Boolean = false

scala> Car.isFast(Car("nissan", 2018, "blue"))
res2: Boolean = true

Scala's type hierarchy

The following picture is taken directly from Scala documentation page.

Scala Inheritance Hierarchy

Scala has one supertype called Any with two sub-types called AnyVal and AnyRef.
The AnyVal is super-type for what in Java we would call primitive types plus some extra fancy Scala types (eg: StringOps for operations on strings and Unit which correstponds to no value)

AnyRef is super-type for all Scala and Java classes.

The Nothing type is type for throw expressions, while Null is the type of value null.

Method chaining example

Let's build a simple counter using Scala classes and learn how to chain methods into a single expression:

    class Counter(val x: Int) {
        def increment:Counter = increment()
        def decrement:Counter = decrement()
        def increment(y: Int = 1) = new Counter(x + y)
        def decrement(y: Int = 1) = new Counter(x - y)
    }

    new Counter(5).increment(2).decrement.increment.x
    // res: Int = 7

In the above example there are 4 instances of the Counter created before returning the final result. Notice how we can use the methods with or without the parenthesis.


[+] Scala series
  • Small steps to Scala 1/20 - the basics
  • Small steps to Scala 2/20 - functions
  • Small steps to Scala 3/20 - arrays
  • Small steps to Scala 4/20 - maps and tuples

  • Disclaimer: This is by no means an original work it is merely meant to serve as a compilation of thoughts, code snippets and teachings from different sources.