A big challenge with DSLs is composability. DSLs are mostly used in silos these days to solve specific problems in one particular domain. But within a single domain there are situations when you need to compose multiple DSLs to design modular systems. Languages like Scala and Haskell offer powerful type systems to achieve modular construction of abstractions. Using this power, you can embed domain specific types within the rich type systems offered by these languages. This post describes a cool example of DSL composition using Scala's type system. The example is a very much stripped down version of a real life scenario that computes the payroll of employees. It's not the richness of DSL construction that's the focus of this post. If you want to get a feel of the power of Scala to design internal and external DSLs, have a look at my earlier blog posts on the subject. Here the main focus is composition and reusability - how features like dependent method types and abstract types help compose your language implementations in Scala.
Consider this simple language interface for salary processing of employees ..
trait SalaryProcessing {
// abstract type
type Salary
// declared type synonym
type Tax = (Int, Int)
// abstract domain operations
def basic: BigDecimal
def allowances: BigDecimal
def tax: Tax
def net(s: String): Salary
}
Salary
is an abstract type, while Tax
is desfined as a synonym of a Tuple2
for the tax components applicable for an employee. In real life, the APIs will be more detailed and will possibly take employee ids or employee objects to get the actual data out of the repository. But, once again, let's not creep about the DSL itself right now.Here's a sample implementation of the above interface ..
trait SalaryComputation extends SalaryProcessing {
type Salary = BigDecimal
def basic = //..
def allowances = //..
def tax = //..
private def factor(s: String) = {
//.. some implementation logic
//.. depending upon the employee id
}
def net(s: String) = {
val (t1, t2) = tax
// some logic to compute the net pay for employee
basic + allowances - (t1 + t2 * factor(s))
}
}
object salary extends SalaryComputation
Here's an implementation from the point of view of computation of the salary of an employee. The abstract type
Salary
has been concretized to BigDecimal
which indicates the absolute amount that an employee makes as his net pay. Cool .. we can have multiple such implementations for various types of employees and contractors in the organization.Irrespective of the number of implementations that we may have, the accounting process needs to record all of them in their books, where they would like to have all separate components of the salary separately from one single API. For this, we need to define a separate implementation for the accounting department with a different concrete type definition for
Salary
that separates the net pay and the tax part. Scala's abstract types allow this type definition overriding much like values. But the trick is to design the Accounting
abstraction in such a way that it can be composed with all definitions of Salary
that individual implementations of SalaryProcessing
define. This means that any reference to Salary
in the implementation of Accounting
needs to refer to the same definition that the composed language uses.Here's the definition of the
Accounting
trait that embeds the semantics of the other language that it composes with ..trait Accounting extends SalaryProcessing {
// abstract value
val semantics: SalaryProcessing
// define type to use the same semantics as the composed DSL
type Salary = (semantics.Salary, semantics.Tax)
def basic = semantics.basic
def allowances = semantics.allowances
def tax = semantics.tax
// the accounting department needs both net and tax info
def net(s: String) = {
(semantics.net(s), tax)
}
}
and here's how
Accounting
composes with SalaryComputation
..object accounting extends Accounting {
val semantics = salary
}
Now let's define the main program that processes the payroll for all the employees ..
def pay(semantics: SalaryProcessing,
employees: List[String]): List[semantics.Salary] = {
import semantics._
employees map(net _)
}
The
pay
method accepts the semantics to be used for processing and returns a dependent type, which depends on the semantics passed. This is an experimental feature in Scala and needs to be used with the -Xexperimental
flag of the compiler. This is an example where we publish just the right amount of constraints that's required for the return type. Also note the semantics of the import statement in Scala that's being used here. Firstly it's scoped within the method body. And also it imports only the members of an object that enbales us to use DSLish syntax for the methods on semantics, without explicit qualification.Here's how we use the composed DSLs with the
pay
method ..val employees = List(...)
// only SalaryComputation
println(pay(salary, employees))
// SalaryComputation composed with Accounting
println(pay(accounting, employees))
No comments:
Post a Comment