By Raoul-Gabriel Urma, Mario Fusco, and Alan Mycroft

In this article, you will learn about DSL with Lambdas, the pros and cons of DSLs, and the different DSL solutions on JVM.

Save 37% off Java 8&9 in Action, Second Edition with code fccurma at manning.com.

A DSL isn’t a general-purpose programming language, but is designed for a specific domain and is targeted to solve problems in that domain. More specifically, a DSL offers a limited vocabulary specific to the domain it addresses, and works at the same level of abstraction as the problems typical of that domain. Your DSL should allow its users to deal only with the complexities of that domain while other lower level implementation details shouldn’t leak through, or at least leak as little as possible, retaining compatibly with the constraints imposed by the syntax of the hosting language.

The main purpose of a good DSL is neither being readable as plain English nor to directly develop the part of the code implementing the business logic. The two reasons that should drive you toward the development of a DSL are:

  • Communication is king – your code should clearly communicate its intentions and be understandable, even for a non-programmer who can check if it matches his knowledge of the domain.

  • Code is written once, but read many times – readability is vital for maintainability, and code that’s easy to read is better than code which is easy to write. You should always code as if the person who maintains your code is a maniacal serial killer who knows where you live.

A well-designed DSL can bring many benefits, but developing and employing one also implies costs and disadvantages. Let’s explore these pros and cons in more detail to take a more informed decision on when to implement a DSL.

Pros and cons of DSLs

As with any other technology or solution in software development, DSLs are no silver bullet. Wrapping the API to manipulate your domain with a DSL can be both an asset and a liability. For this reason, listing the pros and cons of DSLs is a necessary exercise to evaluate if adding one to your project could result in a positive return of the investment. The pros of DSLs are as follows:

  • Conciseness – an API that conveniently encapsulates the business logic allows avoiding repetition and results in more compact code.

  • Readability – using words belonging to the vocabulary of the domain makes the code understandable, even for domain experts, enabling the sharing of the code and the domain knowledge between them and the development team.

  • Maintainability – code written against a well-designed DSL is easier to maintain and modify. This is important for business-related code because it’s the part of the application that will change more frequently.

  • Higher level of abstraction – DSL primitives work at the same level of abstraction of the domain, hiding the details not strictly related to the domain’s problems.

  • Focus – having a language designed for the sole purpose of expressing the rule of the business domain helps to stay focused on that specific part of your code and improve productivity.

  • Separation of concerns – expressing the business logic with a dedicated language makes it easier to keep the business-related code better isolated from the infrastructural part of the application.

Conversely, introducing a DSL into your code base can also have a few disadvantages. Let’s also discuss them:

  • DSL design is hard – even if of limited scope, DSL is a programming language, and language design is one of the most complex tasks in software engineering. It isn’t a job that should be assigned to an inexperienced programmer.

  • Development cost – adding a DSL to your code base is a long-term investment with an upfront cost. This could delay your project in its early stages. Also, maintaining the DSL and letting it evolve with the rest of your project has an additional cost.

  • Additional indirection layer: a DSL wraps your domain model with a further layer that must be as thin as possible to avoid performance problems. Moreover, some performance issues could be caused by poorly designed abstraction, which could lead to exercising more operations than necessary to perform a given task.

  • Another language to learn – nowadays developers are used to employing different languages. Adding a DSL to your project implies that you and your team now have one more language to learn. Even worse, if you decide to have multiple DSLs covering the different areas of your business domain, it could be hard to make them work together in a seamless way because they evolve independently.

  • Hosting language limitations – some general-purpose programming languages are known for having a verbose and rigid syntax, and Java is among them. These languages aren’t good playgrounds to host a DSL. DSLs developed on top of them are constrained by this cumbersome syntax and are often difficult to read. The introduction of lambda expressions in Java 8 offered a new powerful tool to work around this problem.

If you’re working in an environment as rich as the one provided by the JVM, developing a Java 8 based DSL isn’t the only existing solution to build a more readable abstraction on top of your underlying domain model. Before investigating which patterns and strategies we could employ to develop a readable and easy to use DSL in Java 8, let’s quickly explore the other alternatives, clarifying under which circumstances they could be an effective solution.

Different DSL solutions available on the JVM

The first step is classifying these possibilities with the pros and cons related to the choices available to implement a DSL. The most common way of categorizing DSLs, originally introduced by Martin Fowler, is dividing them into internal and external DSLs. Internal DSLs, also known as embedded DSLs, are implemented on top of the existing host language, and external ones are called standalone because they’re developed from scratch with a syntax which is totally independent from the hosting language.

In the remaining part of this article we’ll focus our attention on how to create a Java 8 based and internal DSL. It’s important to mention the available alternatives to take an informed decision about the right technology to use given the characteristics of your project, and maybe more important, the skills of your development team. The JVM gives you a third possibility which is somewhere between an internal and an external DSL: using another general-purpose programming language that runs on the JVM, but is more flexible and expressive than Java, like Scala or Groovy. We’ll refer to this third alternative as polyglot DSL. It’s time to analyze these three possibilities according to increasing levels of power, flexibility, and complexity.

Internal DSL

Because this is an article about Java, when we speak about an internal DSL we clearly mean a Java-based DSL. Historically Java hasn’t been considered a DSL friendly language because its cumbersome and inflexible syntax makes reading a concise, expressive DSL difficult. This issue has been largely mitigated by the introduction of lambda expressions – in fact, using them extensively results in a DSL with a more acceptable signal/noise ratio. To demonstrate this let’s try to print a list of Strings with Java 7 syntax, but using Java 8 API:

 
 List<String> numbers = Arrays.asList("one", "two", "three");
 numbers.forEach( new Consumer<String>() {
     @Override
     public void accept( String s ) {
         System.out.println(s);
     }
 } );
 

In this snippet, we’ve shown the part that carries the signal of the code. All the remaining is syntactic noise that doesn’t provide any additional meaning and is no longer in Java 8. The anonymous inner class can be replaced with a lambda expression:

 
 numbers.forEach(s -> System.out.println(s));
 

or even better, with a method reference:

 numbers.forEach(System.out::println);

That said, if you can use the expressiveness of Java 8 lambdas, and you can live with the outstanding noise and verbosity caused by the Java syntax, there are many advantages in choosing to develop your DSL in plain Java:

  • Your DSL is written in plain Java and it’s compiled with the rest of your code. There’s no additional building cost caused by the integration of a second language compiler or of the tool employed to generate the external DSL.

  • Your development team won’t need to get familiar with a different language or with a complex external tool.

  • The users of your DSL will have all the features normally provided by your favorite Java IDE, like autocompletion and refactoring facilities. Modern IDEs are improving their support to other popular JVM languages, but are still incomparable to what they offer to Java developers. Even worse, if you decide to create your own language you won’t have any IDE support. You could plan to implement the integration between your IDE and your DSL, but this is something usually kept out of the budget and capabilities of the greater part of development teams.

  • In case you need to implement more than one DSL to cover different parts of your domain or multiple domains, there’s no problem composing and making these DSLs interoperable if they’re written in plain Java. This is true even if you decide to use another JVM language, but clearly it won’t be that simple to integrate different custom made external DSLs.

Polyglot DSL

Nowadays there are probably more than 100 different languages running on the JVM. Some of them, like Scala and Groovy, are now quite popular and it’s not difficult to find developers skilled in these languages. Others, like JRuby and Jython, are portings of other well-known languages to the JVM. Finally, there are other emerging languages, like Kotlin and Ceylon, which are gaining traction, mostly because they claim to have features comparable with Scala, but with a lower intrinsic complexity and an easier learning curve. Most of these languages are younger than Java and were designed with a less constrained and verbose syntax. This characteristic has a positive impact when implementing a DSL resulting in a language with a lower ceremony noise.

Scala has a few specific features like currying and implicit conversion that are convenient when developing a DSL. We want to show what is possible with these constructs with a small example. Let’s suppose that we want to build a utility function allowing us to repeat the execution of another function f a given number of times. As a first attempt, we could end up with the following recursive implementation:

 
 def times(i: Int, f: => Unit): Unit = {
   f                         #A
   if (i > 0) times(i-1, f)  #B
 }
 

#A execute the f function

#B if the counter i is positive, decrement it and recursively invoke the times function

Note that in Scala, invoking this function with large values of i won’t cause a stack overflow, as would happen in Java, because Scala has the tail call optimization and the recursive invocation to the times function won’t be added to the stack. You can use this function to execute another function, in this case one that prints “Hello World” three times as it follows.

 
 times(3, println("Hello World"))
 

Currying the times function, or putting its arguments in two groups:

 
 def times(i: Int)(f: => Unit): Unit = {
   f
   if (i > 0) times(i-1)(f)
 }
 

It’s possible to achieve the same result putting the function to be executed multiple times in curly braces:

 
 times(3) {
   println("Hello World")
 }
 

Finally, it’s possible to define an implicit conversion from an Int to an anonymous class having only one function that in turn has as argument the function to be repeated.

 
 implicit def intToTimes(i: Int) = new {      #A
   def times(f: => Unit): Unit = {            #B
     def times(i: Int, f: => Unit): Unit = {  #C
       f                        
       if (i > 0) times(i-1, f) 
     }
     times(i, f)                              #D
   }
 }
  
 

#A Defines an implicit conversion from an Int to an anonymous class …

#B … having only a times function accepting another function f as argument

#C A second times function taking 2 arguments and defined in the scope of the first one

#D Invokes the inner times function

In this way, the user of our small DSL can execute a function printing “Hello World” three times as it follows:

 
 3 times {
   println("Hello World")
 }
 

As you can see, the result doesn’t have any syntactic noise and its meaning is easily understandable, even by a non-developer. Here the number three is automatically converted by the compiler in an instance of a class storing the number three in its i field and having a times function. Then this function is invoked with a dot-less notation passing the function to be repeated.

Obtaining a similar result in Java is impossible, and the advantages in using a more DSL-friendly language are obvious. This choice has some clear inconveniences. First you must learn a new programming language or have somebody in your team already skilled with it. Consider that developing a DSL in these languages generally requires the use of some advanced features, and a superficial knowledge of the new language is normally not enough. Second, it’s necessary to complicate your building process by integrating different compilers to build the source written with two or more languages. Finally, despite the fact that the greater part of the languages running on the JVM claim to be 100% Java compatible, awkward tricks and compromises are often required to make them interoperate with Java. This interoperation sometimes implies a performance loss: for instance, Scala and Java collections aren’t compatible, and when a Scala collection must be passed to a Java function or visa-versa, the original collection must be converted into one belonging to the native API of the target language.

External DSL

The third option is implementing an external DSL. In this case, you’ll need to design a new language from the ground-up with its own syntax and semantics, and then set up a separate infrastructure to parse the new language, analyze the output of the parser, and generate the code executing your external DSL. If this sounds like lots of work, it’s because it is, and the skills necessary to perform these tasks are neither common nor easy to acquire. The tool most commonly used in Java to generate the parser of a textual file is ANTLR. Also, Scala, through its parser combinator library, provides a powerful parsing tool.

As anticipated both these tools are complex and have a steep learning curve. Moreover, designing a coherent programming language from scratch isn’t a trivial task, and the most important barrier to the development of an external DSL is acquiring all the necessary skills. Another common problem is that it’s easy for an external DSL to grow out of control and cover areas and purposes for which it wasn’t originally designed.

Conversely the biggest advantage in developing an external DSL is the practically unlimited degree of flexibility. You have the possibility to design a language that perfectly fits the needs and peculiarities of your domain, and if you do a good job the result will be an extremely readable language specifically tailored to describe and solve the problems of your business. The other positive outcome is the clear separation between your applicative and infrastructural code developed in Java and the business code written using the external DSL. This separation is a double-edged sword because it creates an artificial boundary between the DSL and the host language.

That’s all for this article, for more download the free first chapter of Java 8&9 in Action, Second Edition.