Classes and objects IIIProgramming Practice (CS109)Higher-order methods of collectionsClasses and objects II

Classes and objects II

Objects are the basis of object-oriented programming. In Scala, every piece of data is an object. The type of the object determines what you can do with the object. Classes can contain data (the state of the object) and methods (what you can do with the object). You can think about a class as a blueprint for objects. Once you define a class, you can create objects from the blueprint using the keyword new.

Consider an object as an atomic unit. Clients (that's the term for program code that uses the object) do not care about the implementation of the object, they only use the exposed methods and fields. "Exposed" means here that they are made available by the class for use by clients.

Consistency

A date consists of three attributes: year, month, and day, all of them integers. We could thus store a date as a triple (Int, Int, Int), or using a simple case class with three attributes.

Both methods, however, do not guarantee that our date objects will be consistent. Consistency means that the attributes take on only legal, meaningful values, and that the values of each attribute are consistent with each other. For instance, a day value of 31 is consistent with a month value of 3, but not with a month value of 4. A day value of 29 and a month value of 2 are consistent only if the year value indicates a leap year.

Here is a Date class that ensures that its state is always consistent (days1.scala):

val monthLength = 
    Array(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)

class Date(val year: Int, val month: Int, val day: Int) {
  require(1901 <= year && year <= 2099)
  require(1 <= month && month <= 12)
  require(1 <= day && day <= monthLength(month - 1))
  require(month != 2 || day <= 28 || (year % 4) == 0)
}
The four require statements inside the class body are executed every time an object of type Date is constructed. If the values are okay, nothing happens. Otherwise require throws an exception, and we know immediately that something is wrong. Since the Date object is immutable, the consistent state that is guaranteed when the object is constructed can never be broken and made inconsistent.

It is helpful if objects guarantee that their state is consistent. It makes it possible for functions working with the objects to proceed without error checking, and simplifies debugging since we will notice quickly when something went wrong.

Here are some examples for using Date objects:

scala> var d1 = new Date(2012, 4, 16)
d1: Date = Date@1f6c439
scala> println(d1.year, d1.month, d1.day)
(2012,4,16)
scala> var d2 = new Date(2012, 2, 30)
IllegalArgumentException: requirement failed

Methods

In the past, we had functions that took arguments of a particular type, such as a function date2num that converted a date, represented as year, month, and day, into a day index starting on January 1, 1901.

In object-oriented programming, we prefer to define functions that work on a specific type as a method of that type. One advantage is that it clearly documents the available functions for a given type (for instance, to find out what you can do with a String, you would look at the list of methods of the String class). The main advantage, however, will be the possibility of hiding or protecting information inside the object, as we will see later.

For the moment, let us add methods to convert a Date object into a day index, and to return the day of the week of a Date (days3.scala):

val monthLength = Array(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
val weekday = Array("Monday", "Tuesday", "Wednesday", "Thursday",
		    "Friday", "Saturday", "Sunday")

class Date(val year: Int, val month: Int, val day: Int) {
  require(1901 <= year && year <= 2099)
  require(1 <= month && month <= 12)
  require(1 <= day && day <= monthLength(month - 1))
  require(month != 2 || day <= 28 || (year % 4) == 0)

  // returns the number of days since 1901/01/01 (day 0)
  def dayIndex: Int = {
    val fourY = (365 + 365 + 365 + 366)
    val yearn = year - 1901
    var total = 0
    total += fourY * (yearn / 4)
    total += 365 * (yearn % 4)
    for (m <- 0 until month - 1)
      total += monthLength(m)
    total += day - 1
    if (year%4 != 0 && month > 2)
      total -= 1
    total
  }

  def dayOfWeek: String = weekday((dayIndex + 1) % 7)
}
A method looks like a function definition, but is placed inside the body of the class. The body of the method can refer to the names of the object's fields: Note the use of the fields year, month, and day in the method dayIndex.

A method can only be called if we have a reference to an object of that type available, and so whenever a method is executed, there is always a "current" object. The field names refer to the fields of this "current" object:

scala> var d1 = new Date(2012, 4, 16)
d1: Date = Date@1c97c3e
scala> var d2 = new Date(2000, 1, 1)
d2: Date = Date@165de14
scala> d1.year
res0: Int = 2012
scala> d2.month
res1: Int = 1
scala> d1.dayIndex
res2: Int = 40648
scala> d2.dayIndex
res3: Int = 36159
scala> d1.dayOfWeek
res4: String = Monday
scala> d2.dayOfWeek
res5: String = Saturday

The two methods we wrote here do not take any arguments, not even empty parentheses, and so calling them looks exactly like accessing a field of the object. Scala allows this "uniform access" to fields and methods—this is impossible in Java or C++. This can be a big advantage when you are designing a new class: You can switch between implementing an attribute as a field or as a method without changing the client code.

Printing pretty dates

Every object in Scala (or Java) can be converted to a string, and that is what happens when you look at an object in the interactive mode, or using println: the object is first converted to a string using its toString method:

scala> d1
res5: Date = Date@14096e6
scala> val s = d1.toString
s: String = Date@14096e6
By default, the result of this conversion is not pretty: It contains the name of the class and a hexadecimal number (a number in base 16) that identifies the object on the heap.

We can change this by overriding the default definition of the toString method, like this (days4.scala):

val monthLength = Array(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
val weekday = Array("Monday", "Tuesday", "Wednesday", "Thursday",
		    "Friday", "Saturday", "Sunday")
val monthname = Array("January", "February", "March",
		      "April", "May", "June",
		      "July", "August", "September",
		      "October", "November", "December")

class Date(val year: Int, val month: Int, val day: Int) {
  require(1901 <= year && year <= 2099)
  require(1 <= month && month <= 12)
  require(1 <= day && day <= monthLength(month - 1))
  require(month != 2 || day <= 28 || (year % 4) == 0)

  // returns the number of days since 1901/01/01 (day 0)
  def dayIndex: Int = {
    val fourY = (365 + 365 + 365 + 366)
    val yearn = year - 1901
    var total = 0
    total += fourY * (yearn / 4)
    total += 365 * (yearn % 4)
    for (m <- 0 until month - 1)
      total += monthLength(m)
    total += day - 1
    if (year%4 != 0 && month > 2)
      total -= 1
    total
  }

  def dayOfWeek: String = weekday((dayIndex + 1) % 7)

  override def toString: String = 
    "%s, %s %d, %d".format(dayOfWeek,
			   monthname(month-1),
			   day, 
			   year)
}

Note the keyword override. It is necessary because every Scala object already has a toString method. So we are not adding a new method, we are overriding the previous definition.

With this method, our object looks much prettier:

scala> var d1 = new Date(2012, 4, 16)
d1: Date = Monday, April 16, 2012
scala> val s = d1.toString
s: String = Monday, April 16, 2012
scala> println(d1)
Monday, April 16, 2012

Operators are methods

Let's add a method to compute the number of days between two dates. Since we already have dayIndex, this is very easy to implement (days5.scala):

  def diff(rhs: Date) = dayIndex - rhs.dayIndex

And it works well:

scala> var d1 = new Date(1993, 7, 9)
d1: Date = Friday, July 9, 1993
scala> var d2 = new Date(2012, 4, 9)
d2: Date = Monday, April 9, 2012
scala> d2.diff(d1)
res0: Int = 6849

But wouldn't it be nice if we could write d2 - d1 for the difference between two dates instead of the not-so-pretty d2.diff(d1)?

In Scala, unlike in Java, this is possible. In fact, in Scala, there is no difference at all between operators like + and methods like take—they are all implemented as methods.

Any method that has exactly one argument can be called using operator syntax. For instance, the Array[Int] class has a method take(n) to take the first n elements, and a method :+(el) to add an element at the end. (Both methods return a new array, of course, since arrays cannot change their length.)

Here are examples that show that we can call methods either in method syntax or in operator syntax:

scala> val A = Array(1, 2, 3, 4, 5)
A: Array[Int] = Array(1, 2, 3, 4, 5)
scala> A.take(3)
res0: Array[Int] = Array(1, 2, 3)
scala> A take 3
res1: Array[Int] = Array(1, 2, 3)
scala> A :+ 9
res2: Array[Int] = Array(1, 2, 3, 4, 5, 9)
scala> A.:+(9)
res3: Array[Int] = Array(1, 2, 3, 4, 5, 9)

So we can simply rename our Date method diff to -, and we have difference operator that returns the difference between two dates as a number of days (days6.scala):

class Date(...) {
  // ...
  def -(rhs: Date) = dayIndex - rhs.dayIndex
}
We can now calculate with dates:
scala> val birth = new Date(1993, 7, 9)
birth: Date = Friday, July 9, 1993
scala> val today = new Date(2012, 4, 16)
today: Date = Monday, April 16, 2012
scala> today - birth
res0: Int = 6856

It can be fun to define your own operators, but don't go overboard—it is only useful when it makes the code more readable!

Overloading

Scala, like Java and C++ but unlike C, allows the overloading of method names: It is allowed to have different methods that have the same name, and only differ in the type of arguments they accept. Here is a simple example:

 
def f(n: Int) {
  println("Int " + n)
}
def f(s: String) {
  println("String " + s)
}
f(17)
f("CS109")
The compiler correctly determines that f(17) is a call to the first function, while f("CS109") is a call to the second function. (This example will not work if you type it into the interactive mode—you have to run it as a script!)

We can make use of overloading to add more operators to our Date class. We will allow adding or subtracting a number of days to a date to obtain a new date (days7.scala):

val monthLength = Array(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
val weekday = Array("Monday", "Tuesday", "Wednesday", "Thursday",
		    "Friday", "Saturday", "Sunday")
val monthname = Array("January", "February", "March",
		      "April", "May", "June",
		      "July", "August", "September",
		      "October", "November", "December")

class Date(val year: Int, val month: Int, val day: Int) {
  require(1901 <= year && year <= 2099)
  require(1 <= month && month <= 12)
  require(1 <= day && day <= monthLength(month - 1))
  require(month != 2 || day <= 28 || (year % 4) == 0)

  // returns the number of days since 1901/01/01 (day 0)
  def dayIndex: Int = {
    val fourY = (365 + 365 + 365 + 366)
    val yearn = year - 1901
    var total = 0
    total += fourY * (yearn / 4)
    total += 365 * (yearn % 4)
    for (m <- 0 until month - 1)
      total += monthLength(m)
    total += day - 1
    if (year % 4 != 0 && month > 2)
      total -= 1
    total
  }

  def dayOfWeek: String = weekday((dayIndex + 1) % 7)

  override def toString: String = 
    "%s, %s %d, %d".format(dayOfWeek,
			   monthname(month-1),
			   day, 
			   year)

  def num2date(n: Int): Date = {
    val fourY = (365 + 365 + 365 + 366)
    var year = 1901 + (n / fourY) * 4
    var day = n % fourY 
    if (day >= 365 + 365 + 365 + 59) {
      year += 3
      day -= 365 + 365 + 365
    } else {
      year += (day / 365)
      day = day % 365
      if (day >= 59)
	day += 1
    }
    var month = 1
    while (day >= monthLength(month-1)) {
      day -= monthLength(month-1)
      month += 1
    }
    new Date(year, month, day+1)
  }

  def -(rhs: Date) = dayIndex - rhs.dayIndex

  def +(n: Int): Date = num2date(dayIndex + n)
  def -(n: Int): Date = num2date(dayIndex - n)
}
Note that there are two - operators defined for the Date class. The compiler correctly selects the one we need depending on the type of the right-hand side:
scala> val birth = new Date(1992, 8, 21)
birth: Date = Friday, August 21, 1992
scala> val baekil = birth + 100
baekil: Date = Sunday, November 29, 1992
scala> val today = new Date(2012, 4, 16)
today: Date = Monday, April 16, 2012
scala> today - birth
res0: Int = 7178
scala> today - 7178
res1: Date = Friday, August 21, 1992
scala> birth + 7178
res2: Date = Monday, April 16, 2012

So what about case classes?

In a previous chapter we have learnt about case classes. In this chapter we have started to talk about normal classes. So what's the difference?

The short answer is: Case classes are for small objects that have no hidden state (see the next chapter for some examples of big objects with hidden state).

The long answer is that case classes are different from normal classes in the following ways:

Classes and objects IIIProgramming Practice (CS109)Higher-order methods of collectionsClasses and objects II