Monday, February 22, 2010

DSL : Grow your syntax on top of a clean semantic model

A DSL primarily has two components - a semantic model that abstracts the underlying domain and a linguistic abstraction on top that speaks the dialect of the user. The semantic model is the model of the domain where you can apply all the principles of DDD that Eric Evans espouses. And the linguistic abstraction is a thin veneer on top of the underlying model. The more well abstracted your model is, easier will be the construction of the layer on top of it. Here's a general architecture of a DSL engineering stack :-


It's interesting to observe that the two components of the stack evolve somewhat orthogonally.

The Semantic Model evolves Bottom Up

The semantic model usually evolves in a bottom up fashion - larger abstractions are formed from smaller abstractions using principles of composition. It can be through composition of traits or objects or it can be through composition of functions as well. How beautiful your compositions can be depends a lot on the language you use. But it's important that the semantic model also speaks the language of the domain.

Here's an example code snippet from my upcoming book DSLs In Action that models the business rules for a trading DSL. When you do a trade on a stock exchange you get charged a list of tax and fee components depending on the market where you execute the trade. The following snippet models a business rule using Scala that finds out the list of applicable tax/fee heads for a trade ..

class TaxFeeRulesImpl extends TaxFeeRules {
  override def forTrade(trade: Trade): List[TaxFee] = {
    (forHKG orElse 
       forSGP orElse 
         forAll)(trade.market)
  }
   
  val forHKG: PartialFunction[Market, List[TaxFee]] = { 
    case HKG => 
      // in real life these can come from a database
      List(TradeTax, Commission, Surcharge)
  }
    
  val forSGP: PartialFunction[Market, List[TaxFee]] = {
    case SGP => 
      List(TradeTax, Commission, Surcharge, VAT)
  }
    
  val forAll: PartialFunction[Market, List[TaxFee]] = {
    case _ => List(TradeTax, Commission)
  }

  //..
}


The method forTrade clearly expresses the business rule, which reads almost as expressive as the English version ..

"Get the Hong Kong specific list for trades executed on the Hong Kong market OR Get the Singapore specific list for trades executed on the Singapore market OR Get the most generic list valid for all other markets"

Note how Scala PartialFunction s can be chained together to give the above model an expressive yet succinct syntax.

The Language Interface evolves Top Down

Here you start with the domain user. What dialect does he use on the trading desk ? And then you try to build an interpreter around that which uses the services that the semantic model publishes. I call this thin layer of abstraction a DSL Facade that sits between your DSL script and the underlying domain model and acts as the glue.

It also depends a lot on the host language as to how you would like to implement the facade. With a language like Lisp, macros can come in very handy in designing an interpreter layer for the facade. And with macros you do bottom up programming, bending the host language to speak your dialect.

When you are developing an external DSL, the EBNF rules that you specify act as the DSL Facade for growing your syntax. Within the rules you can use foreign code embedding to interact with your semantic model.

In summary, when you design a DSL, the semantic model is as important as the dialect that it speaks. Having a well designed semantic model is an exercise in designing well-engineered abstractions. And as I mention in my book, the four qualities of good abstractions are minimalism, distillation, extensibility and composability.

2 comments:

Praki Prakash said...

Hi Debasish,

I really liked this post for your mention of the qualities of good abstraction. I am not sure of the sense in which you define distillation, but the remaining three are something I have deeply cared about. It's good to see those principles highlighted for they are not commonly understood and employed.

As I read your example DSL, I couldn't help a feeling of being lost in Scala-specific details. As a scala-illiterate, I can't make sense of that bit of code. I can easily imagine a Lisp version of such rules which would be totally devoid of language-specific doodads except for the parenthesis. I would imagine that a Haskell version would/could be devised which require no language expertise to understand them and hopefully, to write them as well.

Where I am going with this is that, the base language of DSL makes a lot of difference (as you mention in your post). This has been reinforced in my attempts to create a DSL-ish vocabulary for constructing user interfaces in Javascript. Even though, Javascript has some nice expressive power, the syntax gets in the way, leaving me pining for something like Haskell (or Lisp?).

I am quite curious on what you think of this. Which language allows one to create clutter free DSLs and how practical they are. Hopefully, I can find some information in your book.

Thanks

Debasish said...

When looking at a DSL there are 2 things that you need to consider - the surface syntax that your user will be using and the underlying implementation that you as the DSL designer should be writing and (possibly) others will be maintaining. In the snippet that I showed in my post, it's part of a DSL implementation, where you cannot avoid the language influence. The challenge is still to make it business expressive, so that the person who maintains your implementation can understand the code if he knows the domain. I am sure a person who knows Scala will appreciate the expressiveness of the snippet.

The other aspect of the DSL is designing the user facing syntax, which needs to be as much decoupled from the host language as possible. The DSL syntax for which I posted the implementation snippet and mention the complete implementation in my book is one that creates a trade ..

200 discount_bonds IBM
for_client NOMURA on NYSE at 72.ccy(USD)

which pretty much speaks the business only without any Scala specific implementation details. Even if you look at the DSL implementations in Haskell, you will notice that you need to grok Haskell to understand the implementation of the DSL.

Cheers.