Singleton and companion objects |
Sometimes we need to define a class where we plan to create only one single object of that type. Such a class is called a singleton class, and the single object of that type is called singleton (object).
Scala allows us to define such a singleton object by writing a class definition where we replace the keyword class by object.
Such a singleton definition defines a new type like a class does, but only one single object of this type can be created.
object Kermit { println("Kermit is born") var age = 0 def say() { println("It's not easy being green.") age += 1 } }
Note that the println statement is part of the singleton's constructor. The singleton has one field age, and one method say.
We have now defined a class type, and assigned the name Kermit to the only object of this type. The object does not yet exist—the construction happens the first time the object is used (so if you define a singleton and never use it, it will not be constructed at all).
scala> Kermit.say() Kermit is born It's not easy being green. scala> Kermit.say() It's not easy being green. scala> Kermit.age res3: Int = 2 scala> Kermit res4: Kermit.type = Kermit$@2d285de9
Note how Kermit is born automatically the first time we call its say method. Note also that the name Kermit is not a type name, but the name of an object! You can think about the name Kermit as a val-name. It refers to an object on the heap, and its meaning can never change.
Scala programs are executed using the Java Virtual Machine (JVM). The JVM has a purely object-oriented architecture: All code has to be a method of some class, all data has to be stored in the members of objects. So in particular, there are no global variables, and no no global functions (every function has to be a method of some class).
The Scala compiler allows us to write scripts that violate these rules. It does so by automatically generating a "main class" that contains our global variables and functions as fields and methods.
But once we start working on large projects, we need to work with compiled Scala code (see the next chapter), where this is no longer possible. And the rules of the JVM are good rules for large programs anyway!
Let's review our Date class from a previous chapter (date1.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) }
This code is not pure object-oriented code, because it contains the three global variables monthLength, weekday, and monthname.
How can we avoid these global variables?
One idea would be to move the definitions into the Date class (date2.scala):
class Date(val year: Int, val month: Int, val day: Int) { 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") 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) // rest omitted }
This works, but it has a huge disadvantage: We have now made monthLength, weekday, and monthname fields of the Date class. Every time a Date object is created, three arrays are created, and three extra fields are stored in every Date object. In a program that uses a large number of dates, we are wasting a large amount of space.
We really need to store these three arrays only one single time, so the right place to put them is inside a singleton object. We can name this object Date, just like the class—this is not a conflict (date3.scala):
object Date { 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 <= Date.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 += Date.monthLength(m) total += day - 1 if (year % 4 != 0 && month > 2) total -= 1 total } def dayOfWeek: String = Date.weekday((dayIndex + 1) % 7) override def toString: String = "%s, %s %d, %d".format(dayOfWeek, Date.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 >= Date.monthLength(month-1)) { day -= Date.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 I had to change my Date class slightly, by changing monthLength to Date.monthLength, etc.
This is now good code: It is efficient, purely object-oriented, and works well.
However, there is more to this. Imagine that for some reason we don't want clients of our code to see (and possibly modify) the three arrays, so we make the fields of the Date singleton private (date4.scala):
object Date { private val monthLength = Array(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) private val weekday = Array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") private val monthname = Array("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December") }
Surprisingly, this has the following result:
scala> Date.monthLength <console>:8: error: value monthLength in object Date cannot be accessed in object Date scala> val d = new Date(2014, 8, 12) d: Date = Tuesday, August 12, 2014
As expected, we can no longer access the field monthLength of the Date singleton. However, it seems that the Date class has no problem to get the weekday names and month names from the singleton—even though the fields are private!
The reason it still works is because we gave the singleton and the class the same name. This turns them into companions: The singleton is the companion object of the class.
A companion object is allowed to use the private fields and private methods of its companion class, and the class is allowed to use the private fields and method of its companion object.
This makes a companion object the natural place to store data that is related to some class, but only needed once.
Warning: Companion objects will not work if you define them in Scala's interactive mode. The definition of the companion object must be in the same file as the class (either a Scala script or a compiled source file).
Let's now concentrate on the method num2date of the Date class. It converts a day index into a date. Note that it does not use any field of the object—it is actually entirely independent on the object we call it with:
scala> val d1 = new Date(2015, 4, 4) d1: Date = Saturday, April 4, 2015 scala> val d2 = new Date(1901, 1, 1) d2: Date = Tuesday, January 1, 1901 scala> val d3 = new Date(2029, 8, 31) d3: Date = Friday, August 31, 2029 scala> d1.num2date(10000) res1: Date = Saturday, May 19, 1928 scala> d2.num2date(10000) res2: Date = Saturday, May 19, 1928 scala> d3.num2date(10000) res3: Date = Saturday, May 19, 1928
So it's unnatural that this should be a method of the Date object. In fact, this is inconvient: If I want to create a Date object from a given day index, I first have to create another Date object so that I can call its num2date method!
So the natural place for this method is not in the class, but in its companion object. Let's rename it to fromDayIndex, because that makes its full name Date.fromDayIndex, which nicely explains what it does: It creates a Date object from a day index.
This gives us the following code (date5.scala):
object Date { private val monthLength = Array(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) private val weekday = Array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") private val monthname = Array("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December") def fromDayIndex(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 >= Date.monthLength(month-1)) { day -= Date.monthLength(month-1) month += 1 } new Date(year, month, day+1) } } 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 <= Date.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 += Date.monthLength(m) total += day - 1 if (year % 4 != 0 && month > 2) total -= 1 total } def dayOfWeek: String = Date.weekday((dayIndex + 1) % 7) override def toString: String = "%s, %s %d, %d".format(dayOfWeek, Date.monthname(month-1), day, year) def -(rhs: Date) = dayIndex - rhs.dayIndex def +(n: Int): Date = Date.fromDayIndex(dayIndex + n) def -(n: Int): Date = Date.fromDayIndex(dayIndex - n) }
If you are familiar with static methods in Java or C++, you may have wondered why Scala does not seem to have them. The reason is that they are not necessary: What would be a static method in Java is simply a method of the companion object in Scala.
We now introduce a little bit of "syntaxtic sugar": a nice feature that can make programs look nicer.
When you give your class or your object a method whose name is apply, then this method can be called by simply putting parenthesis behind the name of the object (so you do not need to write the method name). This works both for classes and singleton objects, here is an example for a singleton:
scala> object Pikachu { | def apply(s: String) { | printf("Happiness is being %s\n", s) | } | } defined object Pikachu scala> Pikachu.apply("yellow") Happiness is being yellow scala> Pikachu("yellow") Happiness is being yellow
Writing Pikachu("yellow") is simply an abbreviation (syntactic sugar) for writing Pikachu.apply("yellow").
You may have wondered why we could create arrays by writing Array(1, 2, 3). This is the solution: There is a singleton object named Array, and it has an apply method. The same is true for case classes: The compiler automatically creates companion objects with apply methods so that you can create case class objects without using new. Here is the proof:
scala> val a = Array.apply(1, 2, 3) a: Array[Int] = Array(1, 2, 3) scala> val b = List.apply(1, 3, 4) b: List[Int] = List(1, 3, 4) scala> case class Point(x: Int, y: Int) defined class Point scala> val p = Point.apply(3, 4) p: Point = Point(3,4)
We can do the same thing with our Date example. We add an apply method to the companion class (date6.scala):
object Date { // ... def apply(year: Int, month: Int, day: Int): Date = new Date(year, month, day) }
We can now create Date objects without new:
scala> val d1 = Date(2015, 4, 4) d1: Date = Saturday, April 4, 2015
Note that there are now three ways of making Date objects:
Sometimes it's actually useful to make the methods of the companion object the only legal way of creating objects—that is, we forbid clients to use the constructor of the object directly. This can be done by making the constructor private, as follows (date7.scala):
class Date private (val year: Int, val month: Int, val day: Int) { // class body is unchanged }
Since the methods of the companion class can use the private constructor of Date, they still work correctly. But clients can no longer construct objects themselves:
scala> val d1 = Date(2015, 4, 4) d1: Date = Saturday, April 4, 2015 scala> val d2 = Date.fromDayIndex(10000) d2: Date = Saturday, May 19, 1928 scala> val d3 = new Date(2029, 8, 31) <console>:7: error: constructor Date in class Date cannot be accessed
Singleton and companion objects |