Functions compose naturally - hence if your DSL publishes combinators then your client can compose them to form larger structures. But for this composition to be effective, you need to have the proper types for each of them - yes, we are talking about type-safe composition.
In Scala I tend to use the curried syntax a lot when I design combinators. Let’s have a look at an example .. we assume that we have a domain model designed using some paradigm - it can be OO, it can be functional or maybe a mix of the two. When we implement a business rule, we use these model elements, wire them up and present a proper syntax to the user. As an example here's a domain rule for enrichment of a securities trade as it goes through its processing pipeline ..
- get the tax/fee ids for a trade
- calculate tax/fee values
- enrich trade with net cash amount
// get the list of tax/fees applicable for this trade
// depends on the market
val forTrade: Trade => Option[List[TaxFeeId]] = {trade =>
taxFeeForMarket.get(trade.market) <+> taxFeeForMarket.get(Other)
}
// all tax/fees for a specific trade
val taxFeeCalculate: Trade => List[TaxFeeId] => List[(TaxFeeId, BigDecimal)] = {t => tids =>
tids zip (tids ? valueAs(t))
}
val enrichTradeWith: Trade => List[(TaxFeeId, BigDecimal)] => BigDecimal = {trade => taxes =>
taxes.foldLeft(principal(trade))((a, b) => a + b._2)
}
Note each of these combinators return a function instead of a concrete business abstraction. Of course combinators are supposed to return a function only. But my point is that with such a strategy it becomes easier to build larger abstractions out of these smaller ones. And these combinators will be domain facing - each of them implements a snippet of a business rule. Hence take special care to name them appropriately so that you have that ubiquitous language going within your DSL syntax. Here’s how we combine the above to form a larger business rule that implements the “enrichment of trade” functionality ..
// enrichment of trade
// implementation follows problem domain model
val enrich = for {
// get the tax/fee ids for a trade
taxFeeIds <- forTrade
// calculate tax fee values
taxFeeValues <- taxFeeCalculate
// enrich trade with net amount
netAmount <- enrichTradeWith
}
yield((taxFeeIds map taxFeeValues) map netAmount)
// enriching a trade
trade map enrich should equal(…)
.. very concise, very succinct and brings out the domain semantics very explicitly. This composition was possible only because we used the curried syntax in designing our combinators. Haskell has curry-by-default - no wonder it revels in designing of embedded DSLs.
So, is combinators as the building block of DSL syntax a good idea to follow ?
I have been using this strategy for some time now and it has been working out quite nicely for me. It’s not essential that your underlying domain model has to be purely functional. I have been using libraries like scalaz in scala that offers a good hierarchy of typeclasses which help a lot in designing generic combinators that work across an unrelated family of abstractions. In my PhillyETE talk I discussed how functors allows you to map over entities across diverse abstractions like
List
, Option
and Functions
. And once you make your combinators generic, they tend to be adopted within the DSL as part of the ubiquitous language.