Using Generalized Type Constraints - How to Remove Code With Scala 2.8
Join the DZone community and get the full member experience.
Join For FreeI 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
class Trade[I <: Instrument](id: Int, account: String, instrument: I) {ev is the type class which the compiler provides that ensures that we invoke accruedInterest only for CouponBond trades. You can now do ..
//..
def accruedInterest(convention: String)(implicit ev: I =:= CouponBond): Int = {
//..
}
}
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) {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.
//..
def validateInstrumentNotMatured(implicit ev: I <:< FI): Boolean = {
//..
}
}
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 {But how do we use it within the core API that the Trade class publishes ? Type Classes to the rescue (once agian!) ..
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
}
}
class Trade[I <: Instrument](id: Int, account: String, instrument: I) {and we can now use the type classes using our own specific implementation ..
//..
def accruedInterest(convention: String)(implicit ev: I =:= CouponBond, strategy: CalculationStrategy): Int = {
//..
}
}
implicit val strategy = DefaultImplementation("default")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.
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
From http://debasishg.blogspot.com/2010/08/using-generalized-type-constraints-how.html
Opinions expressed by DZone contributors are their own.
Trending
-
Microservices With Apache Camel and Quarkus
-
Auditing Tools for Kubernetes
-
RBAC With API Gateway and Open Policy Agent (OPA)
-
Extending Java APIs: Add Missing Features Without the Hassle
Comments