Monday, October 20, 2008

How Scala's type system works for your Domain Model

When you have your type system working for you and model many of the domain constraints without a single line of runtime checking code, you can save much on the continuous tax payable down the line. I have been working on a domain model in Scala, and enjoying the expressiveness that the type system brings in to the implementation. There is so much that you can do with minimal ceremony, just using the powers of abstract types, abstract vals and self type annotations. In this post, I will share some of my experiences in implementing explicit domain constraints and invariants using Scala's type system and review some of the benefits that we can derive out of it, with respect to code readability, maintenability and succinct expression of the essence of the model.

I am talking about modeling security trades from a brokerage solution stack .. (horribly simplified) ..


object TradeComponent {
  import ReferenceDataComponent._
  /**
   * A trade needs to have a Trading Account
   */
  trait Trade {
    type T <: Trading

    val account: T
    def valueOf: Unit
  }

  /**
   * An equity trade needs to have a Stock as the instrument
   */
  trait EquityTrade extends Trade {
    type S <: Stock

    val equity: S
    def valueOf {
      //.. calculate value
    }
  }

  /**
   * A fixed income trade needs to have a FixedIncome type of instrument
   */
  trait FixedIncomeTrade extends Trade {
    type FI <: FixedIncome

    val fi: FI
    def valueOf {
      //.. calculate value
    }
  }
  //..
  //..
}



All of the above traits can be mixed in with actual service components and provide explicit abstraction over the constrained types that they declare as members. Note we have not yet committed to any concrete type in any of the above abstractions so far, but provided declarative constraints, just enough to model their domain invariants, e.g. a fixed income trade can only be instantiated with an instrument whose type is bounded on the upper side by FixedIncome. Once declared, the compiler will ensure this forever ..

Now we define a helper component that is only applicable for FixedIncome trades .. Note the appropriate bound on the type, explicitly declared to enforce the business rule ..


object TradeComponent {
  //.. as above
  //..

  /**
   * Accrued Interest is computed only for fixed income trades
   */
  trait AccruedInterestCalculatorComponent {
    type T <: FixedIncomeTrade

    val acc: AccruedInterestCalculator
    trait AccruedInterestCalculator {
      def calculate(trade: T)
    }
  }

  /**
   * Implementation of AccruedInterestCalculatorComponent. Does not
   * yet commit on the actual type of the instrument.
   */
  trait AccruedInterestCalculatorComponentImpl
    extends AccruedInterestCalculatorComponent {
    class AccruedInterestCalculatorImpl extends AccruedInterestCalculator {
      override def calculate(trade: T) = {
        //.. logic
      }
    }
  }
  //..
  //..
}



Let us now define a generic service component that provides trading service for all types of trades and instruments. Ignore the details of what the service offers, details have been elided to focus on how we can build expressive domain models using the abstraction capabilities offered by Scala's type system. The generic service still does not commit to any implementation. It uses the services of another component TaxRuleComponent using self type annotations.


object TradeComponent {

  //.. as above
  //..

  /**
   * Generic trading service
   */
  trait TradingServiceComponent {
    type T <: Trade

    val trd: TradingService
    trait TradingService {
      def valueTrade(t: T)
    }
  }

  /**
   * Implementation of generic trading service
   */
  trait TradingServiceComponentImpl extends TradingServiceComponent {
    this: TaxRuleComponent =>
    class TradingServiceImpl extends TradingService {
      override def valueTrade(t: T) = {
        val l = tax.getTaxRules(t.account)
        //.. logic
        t.valueOf
      }
    }
  }
  //..
  //..
}



When you define your type contraints correctly, Scala provides great support for wiring your components together through explicit type and value definitions in the final assembly. The following component assembly uses the Cake pattern that Jonas Boner recommended for implementing dependency injection. We are going to implement a concrete component assembly for fixed income trades. We need to define the concrete type once in the object and all type dependencies will be resolved through the Scala compiler magic. And we need to provide concrete implementations for all of the abstract vals that we have declared above in defining the generic components ..


object TradeComponent {

  //.. as above
  //..

  /**
   * The actual component that will be published and commits on the concrete types and vals
   */
  object FixedIncomeTradeComponentRegistry extends TradingServiceComponentImpl
    with AccruedInterestCalculatorComponentImpl
    with TaxRuleComponentImpl {

      type T = FixedIncomeTrade
      val tax = new TaxRuleServiceImpl
      val trd = new TradingServiceImpl
      val acc = new AccruedInterestCalculatorImpl
  }
  //..
  //..
}



Now that's a lot of domain constraints implemented only through the power of the type system. And these business rules are explicit within the code base and not buried within spaghetti of procedural routines, making your code maintenable and readable. Imagine how many lines of runtime checks we would have to write to implement the same in a dynamically typed language. And, btw, when you express your constraints through the type system, you don't need to write a single line of unit test for their verification - the compiler does that for you. Next time when you wonder how concise or terse your favorite dynamically typed language is, don't forget to figure that count in.

5 comments:

Luc Duponcheel said...

very convincing, indeed!

patrickdlogan said...

This is interesting... it is a long way toward the kind of explanation I have been looking for re: "type-driven programming" with rich type systems as opposed to "test-driven programming" with dynamic languages.

I am still a big fan of the latter and do not fully comprehend the former.

I'd be interested in your "type development" process - without "tests" of some kind, the type system may validate the "type soundness" of your types, but how do you know they are the types you actually *want* to have proven sound?

Debasish said...

Regarding "type development process", I view it the other way. With a language that offers powerful type modeling abilities, I try to model the domain objects as much as possible using the constraints that it offers. In the
example cited in the above post, when I say :

trait AccruedInterestCalculatorComponent {
  type T <: FixedIncomeTrade
  //..
}

it is implied statically by the compiler that I cannot use AccruedInterestCalculatorComponent with any type that is not a subtype of FixedIncomeTrade. This is a huge savings as far as writing code as well as tests are concerned. I do not hav to do any runtime check within my accrued interest calculation methods that I am working with the proper type of trade instance. Similarly I do not have to write any test that checks the same.

Now the problem with types is that no language does it perfect. But still I think in a large model it saves on a lot of time and effort when you get some guarantee from the compiler that it will be writing many such tests for you. The better the type system, the more the coverage. I find Scala's type system really helpful in this regard.

Regarding your question "how do you know they are the types you actually *want* to have proven sound?" :

When I have a powerful type system at my disposal, I try to fit every element of my model with the type system. In case of a rich domain model model entities collaborate with each other and with other objects under a certain set of constraints. e.g. In a trading system, certain operations are meaningful only with Fixed Income and not with Stocks.

The nmore such coonstraints you can put on the type system, the better off you are with respect to static enforcement of domain constraints and lesser number of tests to be written.

patrickdlogan said...

Could you provide another post describing the tests you would typically provide for code like this? e.g. what else do you feel should be said (and automated) about the correctness of this code? And what kind of testing tools fit those needs? Something more like ScalaCheck rather than an xUnit?

Debasish said...

Here's another post that I worked on last evening .. slightly hurried though .. http://debasishg.blogspot.com/2009/08/static-typing-gives-you-head-start.html