Scala Classes and Objects

Classes

Create a Scala project called circuits. Then create a Scala class in the project's src directory called Counter.

A counter is an example of a simple memory circuit. It holds a single integer, count, which can be incremented modulo some limit.

Here's our initial class declaration:

class Counter(val lim: Int) {
  val limit = lim
  var count = 0
  def this() { this(10)}
  def inc { count = (count + 1) % limit}
}

Notes:

·       Counter has two fields, limit, a constant, and count, a variable. Both are private, but Scala generates public getter and setter methods for the variable and a public getter method for the constant. These methods have the same names as the corresponding fields creating the impression that the fields are public.

·       Scala identifies classes with their primary constructors. So we can view this as the declaration of a counter constructor that takes a single parameter, lim. Executing the constructor executes every declaration in the following block.

·       This class also provides an auxiliary constructor (this) which calls the primary constructor with lim = 10.

·       Empty parameter lists need not be shown, except for the auxillary constructor, apparently.

Singletons

We don't need to create and instantiate classes to create objects. If we only need a singleton—i.e. a single object—we can simply declare it using an object declaration.

For example, we only need a single TestCounter object to test our counter class. There are two approaches. We can create a tester with a main method:

object TestCounter {
   def main(args: Array[String]): Unit = {
      // Counter test code goes here
   }
}

An object with a main method is executable. Alternatively, we can create a tester that extends the App object:

object TestCounter extends App {
   // Counter test code goes here
}

App objects are executable. The declaration block of an object extending App is the code that will be executed.

Here is our tester:

object TestCounter extends App {
  val c1 = new Counter(7); val c2 = new Counter; val c3 = new Counter(3)
  for(i <- 1 to 50) {
    c1.inc
    c2.inc
    c3.inc
  } 
 
  println("c1.count = " + c1.count)
  println("c2.count = " + c2.count)
  println("c3.count = " + c3.count)
  c3.count = 500
  println("c3.count = " + c3.count)
  println("c3.limit = " + c3.limit)
}

Here's the output it produces:

c1.count = 1
c2.count = 0
c3.count = 2
c3.count = 500
c3.limit = 3

Notice that the count and limit fields of our counters appear to be accessible to the tester.  Actually, these are calls to auto-generated getters and setters.

Encapsulation and other improvements

Let's try to improve our Counter declaration:

class Counter(val limit: Int = 10) {
  // val limit = lim
  private var count = 0
  // def this() { this(10)}
  def inc { count = (count + 1) % limit}
  def getCount = count
}

Notes:

·       We can use the parameters for the primary constructor as the fields themselves.

·       We can also specify a default value for limit. This eliminates the need for the auxiliary constructor

·       We declared count private, so no more auto-generated setters in getters. Instead, we must provide our own getter.

Here's the new test harness:

object TestCounter extends App {
  val c1 = new Counter(7); val c2 = new Counter; val c3 = new Counter(3)
  for(i <- 1 to 50) {
    c1.inc
    c2.inc
    c3.inc
  } 
 
  println("c1.count = " + c1.getCount)
  println("c2.count = " + c2.getCount)
  println("c3.count = " + c3.getCount)
  // c3.count = 500 illegal now
  // println("c3.count = " + c3.getCount)
  println("c3.limit = " + c3.limit)
}

Here is the output it produces:

c1.count = 1
c2.count = 0
c3.count = 2
c3.limit = 3

Companions

A companion object is a special instance of a class that may contain additional fields and methods.

These additional members can be treated as the static members of the class.

Here's a companion object for the Counter class. It must have the same name as the class and must be declared in the same .scala file:

object Counter {
  var numCounters = 0 // keep track of the number of counters created
  def apply(lim: Int = 10) = { new Counter(lim) }
  def test(c: Counter) {
    for(i <- 1 to 10) c.inc
    println("count = " + c.getCount)
  }
}

We'll increment numCounters in the class/constructor declaration:

class Counter(val limit: Int = 10) {
  private var count = 0
  Counter.numCounters += 1
  def inc { count = (count + 1) % limit}
  def getCount = count
}

Here's our revised test harness:

object TestCounter extends App {
  val c1 = Counter(7); val c2 = Counter(); val c3 = Counter(3);
  Counter.test(c1)
  Counter.test(c2)
  Counter.test(c3)
  println("# counters = " + Counter.getCount)
}

Here's the output produced:

count = 3
count = 0
count = 1
# counters = 3

Notes:

·       Because we defined an apply method, we can call the companion object like a function. (This is Scala's secret to merging functional and object-oriented programming. All functions are objects and the syntax foo(x, y, z) compiles to foo.apply(x, y, z).)

·       Counter.apply invokes the Counter constructor and therefore eliminates the need to call new.

·       We must restore the empty parentheses to the definition of c2, otherwise c2 becomes another name for the companion object.

·       We increment numCounters in the primary constructor in case someone wants to still use new to create counters.

·       Scala has no static variables, constants, or functions. It doesn't need them. Any such thing can be declared in a singleton or companion object.

Generic Classes and Functions

Generic classes contain type parameters:

class Stack[T] {
  val elems = new ArrayBuffer[T](100)
  var sp = 0
  def push(elem: T) {
    elems += elem
    sp += 1
  }
  def top = if (sp > 0)elems(sp - 1)
  def pop { sp -= 1 }
}

Now the same code can be reused to create different types of stacks:

object Stack extends App {
  val s = new Stack[String]
  s.push("One")
  s.push("Two")
  s.push("Three")
  println(s.top)
  s.pop
  println(s.top)
 
  val ss = new Stack[Int]
  ss.push(1)
  ss.push(2)
  ss.push(3)
  println(ss.top)
  ss.pop
  println(ss.top)
}

Notes:

·       The companion object can extend App.