When I was learning to use Ruby on Rails, I was impressed by how the framework made expressing units of time so readble: "5.minutes" or "2.weeks". This was achieved by adding methods to the Numeric class, using Ruby's Open Classes.
As a developer new to Scala, I thought it would be an interesting exercise to implement this feature using The Scala Way: implicit conversions. I'm using the Java Time and Money Library which demonstrates the principles from the Domain-Driven Design book. I wrote the library test-first using the expressive ScalaCheck testing framework, but have yet to use the library in a real application.
I'm posting my work so far in case:
- anyone has feedback or suggestions to improve the code
- anyone can actually use it (and make it better)
I've found the Scala community to be a very helpful and smart bunch of folks - thank you for your advice!
The Code
package com.danielwellman.timeandmoneysugar import com.domainlanguage.time.Duration object RichDurationUnits { implicit def intToRichDurationUnits(value: Int) = new RichDurationUnits(value) } class RichDurationUnits(value: Int) { def milliseconds = Duration.milliseconds(value) def seconds = Duration.seconds(value) def minutes = Duration.minutes(value) def hours = Duration.hours(value) def days = Duration.days(value) def weeks = Duration.weeks(value) def months = Duration.months(value) def quarters = Duration.quarters(value) def years = Duration.years(value) } object RichDuration { implicit def intToRichDuration(value: Duration) = new RichDuration(value) } class RichDuration(value: Duration) { /** * Add two RichDurations using Duration's plus method. * * Note that it is a runtime error to add two Durations of unequal base units, * such as seconds and months. Since Duration itself does not use types to * guard against this problem, this library doesn't either. */ def +(other: Duration) = value.plus(other) }
Usage Examples
Here's an example of using the enhancements to Integers:
scala> import com.danielwellman.timeandmoneysugar._ import com.danielwellman.timeandmoneysugar._ scala> import RichDurationUnits._ import RichDurationUnits._ scala> 5.minutes res0: com.domainlanguage.time.Duration = 5 minutes scala> 2 weeks res1: com.domainlanguage.time.Duration = 14 days
Note also the example of "2 weeks" doesn't use a period at all, since the Scala compiler will try to insert a period for you.
Here's an example of using the + operator with Durations as well as the sugar for Integers:
scala> import RichDuration._ import RichDuration._ scala> 2.days + 5.days res2: com.domainlanguage.time.Duration = 7 days scala> 2.minutes + 2.weeks res3: com.domainlanguage.time.Duration = 14 days, 2 minutes scala> 2.minutes + 2.months java.lang.IllegalArgumentException: 2 months is not convertible to: 2 minutes at com.domainlanguage.time.Duration.assertNotConvertible(Unknown Source) at com.domainlanguage.time.Duration.plus(Unknown Source) at com.danielwellman.timeandmoneysugar.RichDuration.$plus(RichDurationUnits.scala:37) at .( :11) at . ( ) at RequestResult$. ( :3) ...
The last addition exposes a feature of the Time And Money library; you can't sensibly add two minutes and two months. I think it should be possible to use the Scala type system to prevent addition of two unrelated units, but haven't tried to do that yet.
I have found that using spaces can lead to some confusion. For example:
scala> 5 minutes + 2 minutes:11: error: timeandmoneysugar.this.RichDurationUnits.intToRichDurationUnits(5).minutes of type com.domainlanguage.time.Duration does not take parameters 5 minutes + 2 minutes ^
Here I believe the compiler is trying to parse the "+ 2 minutes" expression as an argument to the "minutes" method of RichDuration. In this case, you need to wrap some of the arguments to give the compiler a hint, like so:
scala> (5 minutes) + (2 minutes) res8: com.domainlanguage.time.Duration = 7 minutes
References
Here are some articles I found useful while learning about implicit conversions:
I don't know a thing about the Time & Money library, but if you want to leverage the type system to have a compile time error when you do 2.minutes + 2.months, 2.months shouldn't return a duration. It must be something that can't be added to 2.minutes...
Posted by: Gabriel C | March 08, 2009 at 09:02 PM
@Gabriel This makes sense. My thought was to create two different RichDuration types; one for a basis in seconds, the other in months. Then implement the + method such that you can only add units of the same type.
Posted by: Daniel Wellman | March 09, 2009 at 07:11 AM
I'm really enjoying learning Scala; there is so much it can do, while still staying statically typed. I found your blog from the Cyrus Innovation website; do you use much Scala in your work there?
Posted by: Alex Baranosky | September 29, 2009 at 06:04 PM
@Alex We don't use it on any projects right now, but we're considering getting started by writing new tests for an existing Java project in Scala. It seems like a fairly simple way to ease into using the language.
Posted by: Daniel Wellman | September 29, 2009 at 07:49 PM