Debasish specializes in leading delivery of enterprise scale solutions for various clients ranging from small ones to Fortune 500 companies. He is the technology evangelist of Anshin Software (http://www.anshinsoft.com) and takes pride in institutionalizing best practices in software design and programming. He loves to program in Java, Ruby, Erlang and Scala and has been trying desperately to get out of the unmanaged world of C++. Debasish is a DZone MVB and is not an employee of DZone and has posted 55 posts at DZone. You can read more from them at their website. View Full User Profile

Using Generalized Type Constraints - How to Remove Code With Scala 2.8

08.10.2010
| 6607 views |
  • submit to reddit

I love removing code. The more I remove, the lesser is the surface area for bugs to bite. Just now I removed a bunch of classes, made unnecessary by Scala 2.8.0 type system. Consider this set of abstractions, elided for demonstration purposes ..

trait Instrument

// equity
case class Equity(name: String) extends Instrument

// fixed income
abstract class FI(name: String) extends Instrument
case class DiscountBond(name: String, discount: Int) extends FI(name)
case class CouponBond(name: String, coupon: Int) extends FI(name)

Well, it's the instrument hierarchy (simplified) that gets traded in a securities exchange everyday. Now we model a security trade that exchanges instruments and currencies ..

class Trade[I <: Instrument](id: Int, account: String, instrument: I) {
//..
def calculateNetValue(..) = //..
def calculateValueDate(..) = //..
//..
}

In real life a trade will have lots and lots of attributes. But here we don't need them, since our only purpose here is to demonstrate how we can throw away some piece of code :)

Trade can have lots of methods which model the domain logic of the trading process, calculating the net amount of the trade, the value date of the trade etc. Note all of these are valid processes for every type of instrument.

Consider one usecase that calculates the accrued interest of a trade. The difference with other methods is that accrued interest is only applicable for Coupon Bonds, which, according to the above hierarchy is a subtype of FI. How do we express this constraint in the above Trade abstraction ? What we need is to constrain the instrument in the method.

My initial implementation was to make the AccruedInterestCalculator a separate class parameterized with the Trade of the appropriate type of instrument ..

class AccruedInterestCalculator[T <: Trade[CouponBond]](trade: T) {
def accruedInterest(convention: String) = //.. impl
}

and use it as follows ..

val cb = CouponBond("IBM", 10)
val trd = new Trade(1, "account-1", cb)
new AccruedInterestCalculator(trd).accruedInterest("30U/360")

Enter Scala 2.8 and the generalized type constraints ..

Before Scala 2.8, we could not specialize the Instrument type I for any specific method within Trade beyond what was specified as the constraint in defining the Trade class. Since calculation of accrued interest is only valid for coupon bonds, we could only achieve the desired effect by having a separate abstraction as above. Or we could take recourse to runtime checks.

Scala 2.8 introduces generalized type constraints which allow you to do exactly this. We have 3 variants as:

  • A =:= B, which mandates that A and B should exactly match
  •  
  • A <:< B, which mandates that A must conform to B
  •  
  • A A <%< B, which means that A must be viewable as B
Predef.scala contains these definitions. Note that unlike <: or >:, the generalized type constraints are not operators. They are classes, instances of which are implicitly provided by the compiler itself to enforce conformance to the type constraints. Here's an example for our use case ..
class Trade[I <: Instrument](id: Int, account: String, instrument: I) {
//..
def accruedInterest(convention: String)(implicit ev: I =:= CouponBond): Int = {
//..
}
}
ev is the type class which the compiler provides that ensures that we invoke accruedInterest only for CouponBond trades. You can now do ..
val cb = CouponBond("IBM", 10)
val trd = new Trade(1, "account-1", cb)
trd.accruedInterest("30U/360")

while the compiler will complain with an equity trade ..

val eq = Equity("GOOG")
val trd = new Trade(2, "account-1", eq)
trd.accruedInterest("30U/360")

Now I can throw away my AccruedInterestCalculator class and all associated machinery. A simple type constraint tells us a lot and models domain constraints, and all that too at compile time. Yum!

You can also use the other variants to great effect when modeling your domain logic. Suppose you have a method that can be invoked only for all FI instruments, you can express the constraint succinctly using <:< ..

class Trade[I <: Instrument](id: Int, account: String, instrument: I) {
//..
def validateInstrumentNotMatured(implicit ev: I <:< FI): Boolean = {
//..
}
}
This post is not about discussing all capabilities of generalized type constraints in Scala. Have a look at these two threads on StackOverflow and this informative gist by Jason Zaugg (@retronym on Twitter) for all the details. I just showed you how I removed some of my code to model my real world domain logic in a more succinct way that also fails fast during compile time.

Update: In response to the comments regarding Strategy implementation ..

Strategy makes a great use case when you want to have multiple implementations of an algorithm. In my case there was no variation. Initially I kept it as a separate abstraction because I was not able to constrain the instrument type in the accruedInterest method whole being within the trade class. Calculating accruedInterest is a normal domain operation for a CouponBond trade - hence trade.accruedInterest(..) looks to be a natural API for the context.

Now let us consider the case when the calculation strategy can vary. We can very well extract the variable part from the core implementation and model it as a separate strategy abstraction. In our case, say the calculation of accrued interest will depend on principal of the trade and the trade date (again, elided for simplicity of demonstration) .. hence we can have the following contract and one sample implementation:
trait CalculationStrategy {
def calculate(principal: Int, tradeDate: java.util.Date): Int
}

case class DefaultImplementation(name: String) extends CalculationStrategy {
def calculate(principal: Int, tradeDate: java.util.Date) = {
//.. impl
}
}
But how do we use it within the core API that the Trade class publishes ? Type Classes to the rescue (once agian!) ..
class Trade[I <: Instrument](id: Int, account: String, instrument: I) {
//..
def accruedInterest(convention: String)(implicit ev: I =:= CouponBond, strategy: CalculationStrategy): Int = {
//..
}
}
and we can now use the type classes using our own specific implementation ..
implicit val strategy = DefaultImplementation("default")

val cb = CouponBond("IBM", 10)
val trd = new Trade(1, "account-1", cb)
trd.accruedInterest("30U/360") // uses the default type class for the strategy
Now we have the best of both worlds. We implement the domain constraint on instrument using the generalized type constraints and use type classes to make the calculation strategy flexible.

From http://debasishg.blogspot.com/2010/08/using-generalized-type-constraints-how.html

Published at DZone with permission of Debasish Ghosh, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags:

Comments

King Sam replied on Fri, 2012/02/24 - 10:44am

This kind of post is really, really valuable because it shows the relevance of Scala in a real-world setting (who said that Scala was an academic-only language :-)?).

My only comment would be along what Joe wrote: "its seems as if you now moved the strategy (for calculation) back into the class".
It is fairly likely that on a real-world project doing so will clutter your Trade class and bring on more and more dependencies on that class.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.