Wednesday, May 20, 2009

Overriding Scala def With val

Last time, we created a little toy class hierarchy to demonstrate Scala injection. We also illustrated how Scala's powerful type system can keep us out of trouble. This time, we're going to explore some design tradeoffs that emerge from choosing Scala def or Scala val.

To review, we created an abstract base class that stores masses, and always reports their values in kilograms. We extended that with immutable classes that are initialized with values in various units.

abstract class Mass {
def kilograms: double
}

class Kilograms(kg: Double) extends Mass {
def kilograms = kg
}

class Grams(grams: Double) extends Mass {
def kilograms = grams / 1000.0
}

Suppose that the calculation to convert grams into kilograms was difficult and lengthy. Then the Grams implementation of the kilograms method might get us into trouble, because we'd be repeating that work needlessly every time it was called.

class Grams(grams: Double) extends Mass {
def kilograms: Double = {
Thread.sleep(5000) // pretend to think hard
grams / 1000.0
}
}

The above class constructs instantly, but every time somebody calls the kilograms method on an instance, it takes a long time. This is sad because Grams is immutable. We'd like some way to save the output of the calculation instead of the input.

Let's use the javap tool to peer into what Scala is doing under the hood. The constructor argument grams is called a class parameter in Scala-ese. Class parameters used outside of constructors, as grams is used in the kilograms method, become full fledged private fields of the class. Consider the following (edited) snippet.

$ javap -private Grams
Compiled from "Grams.scala"
public class Grams extends Mass
private final double grams;
public Grams(double);
public double kilograms();

Amazingly, Scala allows us to override the abstract "def kilograms" in mass with a "val kilograms" in Grams. This is a lovely language feature, but it's worth spending a little energy to understand what's going on under the hood.

Let's change our kilograms def into a val in our derived classes. The following class is slow to construct, but each call to kilograms completes instantly.

class Grams(grams: Double) extends Mass {
val kilograms: double = {
Thread.sleep(5000) // pretend to think hard
grams / 1000.0
}
}

Take a moment to digest the tradeoff. The first version is small in memory, containing only one double field, the grams class parameter. It constructs quickly, but each call to kilograms takes a long time. The second version constructs slowly, but all calls to kilograms are quick. We would prefer the first design if we expect the users of the class to call kilograms no more than once, and the second design if we expect the users to call kilograms multiple times on each Grams instance.

In the second design, the grams class parameter appears to be used nowhere but in the constructor itself when the "val kilograms" is defined. So, one might expect that it will not become a real field in the Grams class. Trusty javap confirms this suspicion. Consider the following (again edited) snippet.

$ javap -private Grams
Compiled from "Grams.scala"
public class Grams extends Mass
private final double kilograms;
public Grams(double);
public double kilograms();

Note that under the hood, despite being declared a val in the Scala source code, kilograms is also a method. A moment's reflection(no pun intended) will tell us that it has to be a method. Grams is a concrete class that extends an abstract class with a pure virtual kilograms method. So even thought the Scala source hides it, kilograms is still a method of Grams.

What is that public kilograms method up to? Again we appeal to javap, and learn that it's doing nothing except returning the double stored in the private kilograms field. Just as we might have expected.

public double kilograms();
Code:
Stack=2, Locals=1, Args_size=1
0: aload_0
1: getfield #30; // Field kilograms:D
4: dreturn

The above is much shorter than the previous version, which performed the expensive calculation. Again, we conclude that the criteria to prefer one design over the other rests on the expected usage patterns of our class, as explored above.

We should also ask ourselves whether it's possible to delay the expensive calculation, possibly indefinitely, in case it's never needed. This third design would represent the classic programming tradeoff between space and time, and we'll take it up in a later post.

In summary, we've seen that it's possible to override a Scala def with a Scala val. Under the hood, the override is still implemented by a method. The javap tool is very useful to help us figure out what's going on, and one would do well to understand the design tradeoffs of each approach. Scala's marriage of object oriented programming with functional programming is made in heaven. We can use inheritance and exploit immutability, enjoying the flexibility to make considered design choices.

1 comment:

Unknown said...

Thank you, this is very helpful and detailed