DSL: Grow Your Syntax on Top of a Clean Semantic Model
Join the DZone community and get the full member experience.
Join For FreeA 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.
Opinions expressed by DZone contributors are their own.
Comments