Sunday, November 28, 2010

Day 4: Trying units

The previous two posts have just been about getting a little bit more comfortable with the tooling around Scala. Now it's time to write some code for real. (I am actually writing the code with Emacs Ensime. More on that in a future post.)

If I write code in order to learn a new language, there are actually a couple of rules I observe:

  • I don't write 'Hello world';
  • I want to write code that not only explores some concept, but also proofs its value for real;
  • I want to be able reuse that code for something real in the future.

In this particular case, I decided to explore an area in which I suspect Scala will really shine, which is...

Units


I remember having looked at JSR 275 in the past. It left me with a feeling of admiration on one hand ("interesting from a hacker point of view"), and yet at the same time with a feeling of disappointment ("this is probably the best we will be able to get, *sigh*").

Fact is that dealing with numerical data based on a certain unit (money, temperature, size, coordinates) will never really feel natural. It seemed from first start that Scala would actually be able to solve this pretty naturally.

So last weekend I sat down and gave it a go.

Temperatures


I'm from Europe. We measure our temperatures in degrees Celcius (or if you are in science, in Kelvin). However, if you are from the US, you will be used to Fahrenheit instead. It always hurts my brain to turn degrees Fahrenheit into degrees Celcius and the other way around. Let's see if Scala can make it a little easier.

Fahrenheit and Celcius classes


Let's start by defining Fahrenheit and Celcius as case classes that only embody their value.

case class Celcius(temperature: Double);
case class Fahrenheit(temperature: Double);

Clearly not as much code as you would need in Java. And even creating and instance is easier: Celcius(35) in Scala says exactly the same as the slightly more verbose: new Celcius(35) in Java.

Making it a little bit more readable


Scala by default might be a little bit more compact, but still reading Celcius(35) doesn't read all that well. Perhaps adding a little bit of syntactic sugar will improve it.

To that end, I am introducing a new class Temperature that has two operations: fahrenheit() and celcius(). The first one returns a temperature in Fahrenheit, and the other one a temperature in Celcius. This class is going to serve as an aid in helping me doing what I really want to do, which is to type:

34 fahrenheit

... and get a Fahrenheit temperature of 34 as a result. Basically what needs to happen is this:

34 fahrenheit

should be interpreted as

34.fahrenheit()

... and therefore I need to introduce a fahrenheit() operation to Double. That's not doable just like that. The Temperature is therefore going to serve as an intermediate step. Whenever I type 34 fahrenheit, I want Scala to go look for a type that has a fahrenheit() operation, and see if there is an implicit conversion from Double to an instance of that type.

If I define Temperature like this:

class Temperature(value: Double) {

  def fahrenheit() = Fahrenheit(value);
  def celcius() = Celcius(value);

}

... then the only thing I need to do to make it work is define an implicit conversion:

implicit def doubleToTemperature(value: Double) = new Temperature(value);

... and I'm in business. If now I type "34 fahrenheit", this is what the REPL tell me:

scala> 34 fahrenheit
res8: org.scalateral.sample.units.Fahrenheit = 34.0 °F

That is, it will probably print the value slightly differently. I just added different implementations of toString to the definitions of Celcius and Fahrenheit:

case class Fahrenheit(temperature: Double) {
  override def toString() = temperature.toString() + " \u00B0F";
}

... just to get something a little bit more human friendly.

Comparing temperatures


Although we got something going, it really isn't all that helpful yet. First of all, what's the point of an immutable temperature if all you can do is extract its value from it? It would be clearly way more sensible to define some operations on it as well.

The first thing I want to do is compare temperatures. The current temperature classes (Celcius and Fahrenheit) are not 'comparable' or - in Scala terms - ordered. So let's make them ordered. (I will stick to the Fahrenheit example, but as you can imagine, the Celcius example is exactly the same.)

case class Fahrenheit(temperature: Double) extends Ordered[Fahrenheit] {
  override def toString() = temperature.toString() + " \u00B0F";
  override def compare(that: Fahrenheit): Int = temperature.compare(that.temperature);
}

Because of this I can now type this:

scala> (34 fahrenheit) > (25 fahrenheit)
res12: Boolean = true

No big shakes. This is actually no more than what you would expect. However, it would also be nice to compare temperatures using different units. Like, checking if a temperature in Celcius is higher than a certain temperature in Fahrenheit.

In order to be able to do that, there would be two options. The first option would be to implement a toCelcius on Fahrenheit, and a toFahrenheit on Celcius. However, I am going for the second option, which is IMHO a little bit more extensible. (Adding a new unit of temperature would be easier.) I am going to add two more implicit conversions:

implicit def fahrenheitToCelcius(value: Fahrenheit) 
  = Celcius((value.temperature - 32) / 1.8);
  
implicit def celciusToFahrenheit(value: Celcius) 
  = Fahrenheit((value.temperature * 1.8) + 32);

Once I've done this, comparing temperatures based on different units now all of sudden starts to make sense:

scala> (34 fahrenheit) < (32 celcius)   
res13: Boolean = true
The same goes for operations that take Fahrenheit. I can now simply pass in a temperature in Celcius, and still get valid results:
scala> def printFahrenheit(value: Fahrenheit) = println(value);
printFahrenheit: (value: org.scalateral.sample.units.Fahrenheit)Unit

scala> printFahrenheit(-34 celcius)                            
-29.200000000000003 °F

Conclusion

If you are working with units, then Scala can be a life-saver. With just a little bit of work, Scala allows you to write code that is just way more readable than its Java counterpart.

Word of caution

If you happen to prefer a more natural implementation of toString(), like I do, then you need to be aware of the fact that the Scala REPL will by default use platform encoding for printing to the console. That means that - unless you explicitly override it - on MacOs it will use MacRoman. Unfortunately, MacRoman doesn't have the degree symbol in its character table. As a consequence, you will get the platform specific representation of a non-printable character.

In order to get something a little bit more readable, pass -Dfile.encoding=UTF-8 on the commandline. (In my case, I run the REPL from SBT. So I start SBT like this: java -Dfile.encoding=UTF-8 -jar ~/local/sbt/sbt-launch-0.7.5.RC0.jar).

No comments:

Post a Comment