Classes and objects

Classes and objects are programming constructs that enable us to abstract and group together data and related functionality into convenient design units both at a conceptual level (classes) and in terms of concrete runtime organization (objects instantiated from classes).

Let us consider one simple example to recall how to define classes and work with them.

Suppose we need to work with numerical values in an application so that we need to build derived values from existing values.

We can support such an application in programming terms by first declaring an abstract base class that enables us to read a numerical value and nothing else. (For simplicity, let’s assume all numerical value are Int values.

Now, let us declare such a base class with a single method, read, that will be used to get the numerical value (copy and paste this to the console to play yourself!):

abstract class Value:
  def read: Int
end Value

Note that the method read, is declared, but not implemented. This is allowed in abstract classes.

An abstract class in itself does not enable us to do anything unless we extend it with concrete classes that supply implementations to the abstract methods.

Let us implement a value that evaluates to a constant that is given at construction time when we instatiate the class to a concrete object.

Observe how we declare the class to extend the abstract class and provide a concrete implementation to the abstract method:

class ConstantValue(c: Int) extends Value:
  def read = c
end ConstantValue

We can now start instantiating Value-objects at the console and read their values:

scala> val v = ConstantValue(123)
v: ConstantValue = ConstantValue@4bd3db5c

scala> v.read
res: Int = 123

Beyond constant values, we can of course create a class whose values are variable and can be set (written by a write method) after the object is created. Let us introduce such a class:

class VariableValue(c: Int) extends Value:
  protected var _myVal = c
  def write(v: Int) =
    _myVal = v
  def read = _myVal
end VariableValue

The variable _myVal now holds the current value of the variable value. It is declared protected to restrict access to VariableValue methods (and to inheriting classes).

We observe that we can indeed now write values to a VariableValue-object and read the new value after a write:

scala> val v = VariableValue(456)
v: VariableValue = VariableValue@5aef577f

scala> v.read
res: Int = 456

scala> v.write(111)

scala> v.read
res: Int = 111

Finally, let us create a class that reports the sum of two values. Observe how we use recursion on the two values v1 and v2 to get the sum:

class SumValue(v1: Value, v2: Value) extends Value:
  def read = v1.read + v2.read
end SumValue

At the console we observe that the value of a SumValue-object dutifully evaluates to the sum of the two component values as they evolve with writes:

scala> val v1 = VariableValue(10)
v1: VariableValue = VariableValue@2b35b5d9

scala> val v2 = VariableValue(20)
v2: VariableValue = VariableValue@e0a1ba9

scala> val s = SumValue(v1,v2)
s: SumValue = SumValue@52de94b8

scala> s.read
res: Int = 30

scala> v1.write(0)

scala> s.read
res: Int = 20

scala> v2.write(0)

scala> s.read
res: Int = 0

Note that the value of the sum changes as we update variable values, as expected in our application. This is because read is a method and implemented such that it reads the variable names every time it is being evaluated (and those read methods do the same).

We could now build arbitrarily complex sums of values consisting of constant and variable values and previous sum values. For example:

scala> val x = VariableValue(0)
val x: VariableValue = VariableValue@2a259f6f

scala> val y = VariableValue(0)
val y: VariableValue = VariableValue@4ad3969

scala> val z = VariableValue(0)
val z: VariableValue = VariableValue@2211e731

scala> val xyzSum = SumValue(x,SumValue(y,z))
val xyzSum: SumValue = SumValue@7f894a6f

scala> xyzSum.read
val res: Int = 0

scala> x.write(10)

scala> xyzSum.read
val res: Int = 10

scala> y.write(40)

scala> xyzSum.read
val res: Int = 50

scala> z.write(50)

scala> xyzSum.read
val res: Int = 100

Accordingly, we could extend functionality further by introducing, say, negated values, difference values, product values, and so forth.

This will be pursued in the exercises in the context of polynomials.