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
}