Monday, June 13, 2011

Composing Heterogeneous DSLs in Scala

When we use DSLs to model business rules, we tend to use quite a few of them together. We may use a DSL for computing date/time, another one for manipulating money with currency, and a few others for implementing the actual rules of the domain. And not all of them will be developed by us - third party DSLs play an equally important role out here.

But when we use a bunch of heterogeneous DSLs, our application needs to be flexible enough to adapt to the independent evolution paths of each of them. We need to be able to embed entire DSLs within our application structure and yet not be coupled to the implementation of any of them.

In this blog post, I will demonstrate how we can organize heterogeneous DSLs hierarchically and achieve the above goals of keeping individual implementations decoupled from each other. This is also called representation independence and is yet another strategy of programming to an interface. I will use Scala as the implementation language and use the power of Scala's type system to compose these DSLs together.

The most interesting part of this implementation will be to ensure that the same representation of the composed DSL is used even when you extend your own DSL.

We are talking about accounts and how to compute the balance that a particular account holds. We have a method balanceOf that returns an abstraction named Balance. No idea about the implementation details of Balance though ..

trait Account {
  val bal: Balances
  import bal._

  def balanceOf: Balance

Account is part of our own DSL. Note the abstract val Balances - it's a DSL which we embed within our own abstraction that helps you work with Balance. You can make a Balance abstraction, manipulate balances, change currencies etc. In short it's a utility general purpose DSL that we can frequently plug in to our application structure. Here we stack it hierarchically within our own DSL.

Here's how Balances look ..

trait Balances {
  type Balance

  def balance(amount: Int, currency: String): Balance
  def amount(b: Balance): Int
  def currency(b: Balance): String
  def convertTo(b: Balance, currency: String): Balance

Note Balance is abstracted within Balances. And we have a host of methods for manipulating a Balance.

Let's now have a look at a sample implementation of Balances, which also concretizes a Balance implementation ..

class BalancesImpl extends Balances {
  case class BalanceImpl(amount: Int, currency: String)
  type Balance = BalanceImpl

  def balance(amount: Int, currency: String): Balance = {
    BalanceImpl(amount, currency)

  def amount(b: Balance) = b.amount
  def currency(b: Balance) = b.currency

  def convertTo(b: Balance, toCurrency: String): Balance = {
    BalanceImpl(b.amount * 2, toCurrency)

Note Balances is a complete DSL totally decoupled from our own DSL that has the Account abstraction.

Now on to some concrete Account implementations ..

trait BankAccount extends Account {
  // concrete implementation of Balances
  val bal = new BalancesImpl

  // object import syntax
  import bal._

  // dummy implementation but uses the Balances DSL
  override def balanceOf = balance(10000, "USD")
object BankAccount extends BankAccount

It's a bank account that uses a concrete implementation of the Balances DSL. Note the balanceOf method uses the balance() method of the Balances DSL accessible through the object import syntax.

Now the fun part. My BankAccount uses one specific implementation of Balances. I would like to add a few decorators to my account, which will have richer versions of the API implementation. How do I ensure that all decorators that I may define for BankAccount also get to use the same DSL implementation for Balances?

Here's how .. Not only do we compose decorator and decoratee abstractions hierarchically, we use Scala's singleton type to ensure that the same representation of the Balances DSL gets to flow from the decoratee to the decorator.

// decorator
trait InterestBearing extends Account {
  // decoratee
  val semantics: Account

  // singleton type pulls the same representation from up
  val bal: semantics.bal.type

  // object import ensures that any method of
  // Balances that we use below comes from the same singleton type
  import bal._

  def interest: Int = 100 // dummy implementation

  override def balanceOf = {
    val b = semantics.balanceOf
    balance(amount(b) + interest, currency(b))

So we have done a hierarchical composition of two heterogeneous DSLs and ensured that a single representation of one DSL is used uniformly within the other even in the face of extensions and decorations. The process has been made easy by the power of Scala's static type system.

Now we can have a concrete instance of an interest bearing bank account as a single Scala module ..

object InterestBearingBankAccount extends InterestBearing {
  val semantics = BankAccount
  val bal: semantics.bal.type = semantics.bal

No comments: