C++ itself is complex enough. Adding domain specific languages can seem like yet another addition to that complexity. So why would we use a DSL instead of a normal library?
This post is triggered by a tweet I got yesterday as a reaction to my rationale for this blog:
@arne_mertz Wouldn’t embedding DSL’s complicate things though? seems like a direct counter to your point of simplifying C++
— Reegan Layzell (@rtlayzell) March 6, 2016
This is indeed a valid question. C++ is a complex language, and a developer already has to keep a lot of stuff in mind. A domain specific language is yet another thing to learn and keep in mind – or so it would seem.
Levels of abstraction
One of the reasons for C++’s apparent complexity is that it gives us access to different levels of abstraction. We can work on the relatively high level of classes, modules and libraries, but we can also drill down to the point where we use bit manipulations or even inline assembler.
The key to manage the language and keep in check the beast that is C++, is choosing the right level of abstraction. The only reason why we have functions, classes and higher level abstractions is that we can concentrate on the crucial parts of our algorithms.
We tuck away the very low levels of machine code and assembler behind the abstraction of a programming language. We hide the lower levels of our program logic behind class interfaces and libraries. But what do we do when we have to use multiple classes and maybe even libraries in a way that becomes hard and complex to manage?
Domain specific languages are just another level of abstraction
Choosing a level of abstraction is a tradeoff between the complexity of the code we have to write and the freedom of the functionality at our disposal. If we don’t need the freedom a complex set of classes gives us, because we only need to use them for a constrained set of actions, then we can write another layer of abstraction that makes the use of that set of actions simpler and more comfortable.
Such a layer of abstractions can be a normal library or framework or only a single class or function. It can also be a set of functionality that has a special syntax and a more natural feel for its use in the problem domain. The latter is what we call a domain specific language.
At first sight it may seem that there is a penalty to pay for using a DSL. After all, we have to implement it, unless someone else already did, and we surely have to learn a whole new language.
But it’s not that bad. DSLs are usually rather small languages, so there is not that much to learn. In addition, an embedded DSL has to obey the C++ syntax, so it can not be too different. If we used a library or framework instead, we would have to learn ow to use that one too.
A similar comparison can be made for the implementation cost: a small embedded DSL with constrained functionality does not need to have a higher implementation cost than a normal library that does the same. Actually, the definition of what is a DSL and what is not varies and can be quite blurry, so there can be cases where you can’t tell if a framework is a DSL or just an ordinary set of classes and functions.
It’s a tradeoff
While the drawback for an embedded DSL is usually small, it is not necessarily zero. If we use an external DSL, the penalty in form of implementation cost can be significant, and the increased freedom to choose a syntax can result in a steeper learning curve. Reading external files, parsing and interpreting the content can be a complex topic in itself.
Nevertheless domain specific languages can bring a huge simplification. If they are done right, translating business rules from external documents to code can become a no-brainer. Customers, business analysts and testers may be able to read and understand the DSL.
I once have showed a business analyst that had reported a bug the embedded DSL code responsible for the wrong result and she could immediately point out the error. I was implementing a parser for an external version of that DSL anyways, so later I taught the team how to read and write it and gave them a tool to maintain it themselves. (Sorry guys, more work for you).
DSLs may add complexity to our code, but that addition may not always be as big as we think at first. If we take into account the complexity of the lower level code we don’t have to use any more, the result may well be an improved and simpler code base.
P.S.: You may have used a DSL already without really noticing. It is an external DSL, but it has very terse and short code, so we usually don’t load it from files but rather use strings directly in our code. The parser and interpreter are actually part of the standard library. You can find them in the header <regex> 😉
Can you please suggest some nicely done DSLs to have a look at?
It depends on what you mean by “nicely done”. This could be nicely designed, so the DSL is intuitive and easy to use. It could also be nicely implemented interpreters/compilers etc. . The former is far more important.
I consider for example Cucumber and Markdown to be well designed external DSLs. I don’t know too many C++ embedded DSLS that are publicly available. You could consider the shift operators in the iostream library as minimal DSL, depicting “shove something into the stream”. The use of the binary or operator in the upcoming ranges library resembles Unix pipe persons which could also be interpreted as a micro DSL.
A really large embedded DSL is part of Boost.Spirit. I like the part off it that resembles EBNF notation as closely as possible for C++ operator overloading.
The problem is that C++ language restrictions can get you only 90% there for an arbitrary “nice” DSL. So in my opinion there are good but not 100% nice DSLs with the current C++ languages.
Boost.Spirit and (abandoned?) Blitz++ are obvious example and arguable the first DSLs in C++.
Other examples are Boost.Phoenix (originated as part of Boost.Spirit). Boost.uBlas is a DSL to some extend. Another one is NT2, which (will?) is a serious MATLAB-based DSL for matrices.
The impression right now is that if you want to implement a DSL in C++ correctly you have to use Boost.Proto. (although I never managed myself to pass the stage of the most elementary examples.)