Class Hierarchies and Dynamic Binding - rohgar/scala-principles-1 GitHub Wiki

Class Hierarchies

For class hierarchies we will be taking the example of IntegerSets.

Abstract Classes

These are the classes which are missing an implementation i.e just have the method signature. Consequently these classes cannot be instantiated using new.

abstract class IntSet{
    def incl(x: Int): IntSet
    def contains(x: Int): Boolean
}

Class Extension

Like in Java, classes and abstract classes can be extended by using the extends keyword.

Here we will implement the set as a Binary Tree. We maintain the invariant that the nodes on the right hand side of any node have higher values than the nodes on the left hand side of that node. This will allow us to implement the contains method easily.

A binary tree is made of nodes, where each node contains a "left" pointer, a "right" pointer, and a data element. The "root" pointer points to the topmost node in the tree.

An "empty" set can be implemented as below:

class Empty extends IntSet {
    def incl(x: Int): IntSet = new NonEmpty(x, new Empty, new Empty)
    def contains(x: Int): Boolean = false
    override def toString() = "."
}

A "non-empty" set can be implemented as below:

class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet {
    def contains(x: Int): Boolean =
        if (x < elem) left contains x
        else if (x > elem) right contains x
        else true

    def incl(x: Int): IntSet =
        if (x < elem) new NonEmpty(elem, left incl x, right)
        else if (x > elem) new NonEmpty(elem, left, right incl x)
        else this

    override def toString() = "{" + left + elem + right + "}"
}

In this particular implementation of NonEmpty set, whenever incl() is called to include a new value, a new branch of tree is created. These type of data structure are called as Persistent Data Structures, as even when changes are made, the old version of the data structure stays.

Thus here, the Empty and NonEmpty classes extend the class IntSet. This implies that the types Empty and NonEmpty conform to the type IntSet: an object of type Empty or NonEmpty can be used wherever an object of type IntSet is required.

  • IntSet is called the superclass of Empty and NonEmpty.
  • Empty and NonEmpty are subclasses of IntSet.
  • In Scala, any user-defined class extends another class. If no superclass is given, the standard class Object in the Java package java.lang is assumed.
  • The direct or indirect superclasses of a class C are called base classes of C. So, the base classes of NonEmpty are IntSet and Object.

Implementation and Overriding

The definitions of contains and incl in the classes Empty and NonEmpty implement the abstract functions in the base class IntSet.

It is also possible to redefine an existing, non-abstract definition in a subclass by using override. Eg.

abstract class Base {                                 class Sub extends Base {
    def foo = 1                                           override def foo = 2
    def bar: Int                                          def bar = 3
}                                                     }

Object Definitions

Objects are singleton objects i.e. single instance. No other objects can be created. These can be used directly and don't need the new keyword for instantiation.

In the IntSet example, one could argue that there is really only a single empty IntSet. So it seems overkill to have the user create many instances of it. We can express this case better with an object definition.

object Empty extends IntSet {
    def contains(x: Int): Boolean = false
    def incl(x: Int): IntSet = new NonEmpty(x, Empty, Empty)
}

Programs

It is possible to create standalone applications in Scala. Each such application contains an object with a main method. Eg:

object Hello {
    def main(args: Array[String]) = println(”hello world!”)
}

Once this program is compiled, you can start it from the command line with:

> scala Hello

Dynamic Binding

Static vs Dynamic Binding

In the most general terms, static binding means that references are resolved at compile time.

Animal a = new Animal();
a.Roar(); // The compiler can resolve this method call statically.

Dynamic binding means that references are resolved at run time.

public void MakeSomeNoise(object a) {
 // Things happen...
 ((Animal) a).Roar(); // You won't know if this works until runtime!
}
'''

Object-oriented languages (including Scala) implement dynamic method dispatch. This means that the code invoked by a method call depends on the runtime type of the object that contains the method.