More on classes in Scala

Classes and objects in Scala

Recall our DJData class from last time (which I’ve renamed to RockDJData for the purposes of this lecture):

class RockDJData(val djName: String) {

  var numCallers: Int = 0
  var queue: List[String] = List()

  def request(caller: String, song: String): String = {
    queue = queue :+ song
    numCallers += 1
    if (numCallers % 1000 == 0) {
      "Congrats, " + caller + "! You get a prize from " + djName + "!"
    } else {
      "Cool, " + caller
    }
  }
}

We can make an instance of this class and experiment with its functionality in the REPL:

scala> val djd = new RockDJData("DJ Doug")
scala> djd.request("Alex", "Stairway to Heaven")
"Cool, Alex"
scala> djd.queue
List("Stairway to Heaven")

Notice that we put new before RockDJData. Whenever we create an object in Scala, we’ll have to write new before the object name. Otherwise, our class seems to behave much like its Python equivalent!

We can also add a play method, to play the next song from the queue:

class RockDJData(val djName: String) {

  var numCallers: Int = 0
  var queue: List[String] = List()

  def request(caller: String, song: String): String = {
    queue = queue :+ song
    numCallers += 1
    if (numCallers % 1000 == 0) {
      "Congrats, " + caller + "! You get a prize from " + djName + "!"
    } else {
      "Cool, " + caller
    }
  }

  def play(): String = {
    val song = queue.head
    queue = queue.tail
    song
  }   
}

Scala lists have Pyret-style head and tail methods, to get the first element of the list and the rest of the elements of the list respectively.

Access modifiers

In Python, we talked a lot about an important principle of object-oriented program: encapsulation of data. We learned that as long as an object’s state is only accessed via methods it defines–rather than by directly accessing its fields–we can feel free to change its implementation without needing to change any of the calling code. For instance, we don’t really need to access numCallers and queue from outside the class: we can access them via the request method.

Unlike Python, Scala gives us a way to actually enforce this access pattern. We can make the fields private:

class RockDJData(private val djName: String) {

  private var numCallers: Int = 0
  private var queue: List[String] = List()

  def request(caller: String, song: String): String = {
    queue = queue :+ song
    numCallers += 1
    if (numCallers % 1000 == 0) {
      "Congrats, " + caller + "! You get a prize from " + djName + "!"
    } else {
      "Cool, " + caller
    }
  }

  def play(): String = {
    val song = queue.head
    queue = queue.tail
    song
  }
}

Scala won’t let us build programs that access private fields from outside a class.

Testing Scala programs

First, let’s make one change to make our program easier to test:

class RockDJData(private val djName: String, private val prizeCaller: Int = 1000) {

  private var numCallers: Int = 0
  private var queue: List[String] = List()

  def request(caller: String, song: String): String = {
    queue = queue :+ song
    numCallers += 1
    if (numCallers % prizeCaller == 0) {
      "Congrats, " + caller + "! You get a prize from " + djName + "!"
    } else {
      "Cool, " + caller
    }
  }

  def play(): String = {
    val song = queue.head
    queue = queue.tail
    song
  }
}

We’re using a library called ScalaTest to write tests for our Scala programs. We’ll put the following in a test file (see the lecture capture for details about creating a test file):

import org.scalatest.funsuite.AnyFunSuite

class DJDataSuite extends AnyFunSuite {
  test("request method and prizeCaller") {
    val djd = new RockDJData("Alex", 2)
    val res1 = djd.request("Doug", "Stairway to Heaven")
    val res2 = djd.request("Doug", "Stairway to Heaven")
    assert(res1.contains("Cool"))
    assert(!res1.contains("Congrats"))

    assert(!res2.contains("Cool"))
    assert(res2.contains("Congrats"))
  }


  test("play method") {
    val djd = new RockDJData("Alex")
    intercept[NoSuchElementException] {
      djd.play()
    }
    // assert things about the exception
    djd.request("Doug", "Stairway to Heaven")
    djd.request("Doug2", "Wuthering Heights")
    assert(djd.play() == "Stairway to Heaven")
    assert(djd.play() == "Wuthering Heights")
  }

}

Using intercept, we can ensure that the play method throws an exception when we’d expect.