Measure and improve these characteristics in your DSLs The value of a domain-specific language lies in how well it captures and expresses a particular professional jargon, which in turn depends on the related concerns of fluency and context. In this second article in his series on DSLs, Venkat Subramaniam demystifies these essential characteristics of DSLs with examples based on real-world APIs such as EasyMock and Guice. He concludes with an iterative example in building a fluent, context-aware DSL using Groovy. When domain experts communicate with each other, you may notice that they do not always use complete, grammatically correct sentences. Often, they use a specific technical or professional jargon, which allows them to communicate effectively. The terseness and specificity of jargon can be read in this partial transcript from the first moon landing: ... 102:45:44 Aldrin: Okay. Engine Stop. 102:45:45 Aldrin: ACA out of Detent. 102:45:46 Armstrong: Out of Detent. Auto. 102:45:47 Aldrin: Mode Control, both Auto. Descent Engine Command Override Off. Engine Arm, Off. 413 is in. 102:45:57 Duke: We copy you down, Eagle. 102:45:58 Armstrong (on-board): Engine arm is off. (Pause) Houston, Tranquility Base here. The Eagle has landed. ... The above conversation makes little sense to those who don’t share the experience and terminology of astronauts landing on the moon; but still you can detect the fluency of the communication. According to one definition, “Fluency indicates a very good information processing speed; i.e., very low average time between successively generated messages.” While typically applied to language speakers, the term fluent can also apply to a language itself. The fluency of a language can be measured by how quickly and easily it allows users to process information. Jargon is a key aspect of fluency in many professional domains. If you want to communicate well with other professionals, you need to be fluent in the professional jargon. The specialized terseness of jargon is a handy tool for communicating within a given domain of expertise: it supports a higher signal-to-noise ratio. Martin Fowler and Eric Evans use the term fluent interface or humane interface to describe more “language-like” interfaces, or interfaces well designed for human use. Fluency is a design characteristic of DSLs. A fluent DSL is highly readable and flows naturally for users in its domain. Examples of fluency It might help to take a look at some examples of fluency in programming languages and APIs. For instance, let’s see how we loop through a collection of names in different languages. The traditional syntax in Java would be as follows: Listing 1. Looping through a collection in Javafor(int i = 0; i < names.size(); i++) { String name = (String) name.get(i); //... } If you are like me, you want to scream when someone shows you such banal syntax. The index variable i is redundant, especially if your real intent is to work with the elements of the collection. Another approach would be to use the iterator to traverse the collection, as follows: Listing 2. Iterating through a collection in JavaIterator iter = name.iterator(); while(iter.hasMore()) { String name = (String) iter.next(); //... } Unfortunately, this approach does not seem especially fluent, either. Once you process an element, you have to explicitly push the iterator to the next element. Fortunately, Java 5 introduced a more fluent syntax to loop through the elements of a collection: Listing 3. Fluent looping in Java 5for(String name : names) { // ... } The above syntax is simple, fluent, easy to understand and work with; it has less ceremony. Groovy speaks Groovy is a more fluent language than Java: it was designed that way. For instance, Groovy’s internal iterators allow you to provide code blocks that execute in the context of each element of a collection. So, in Groovy you could use the each method to iterate over the elements of a collection, like so: Listing 4. Easy iterating with Groovy’s each methodnames.each { name -> //... } Now that’s fluent! Fluent APIs Once you are looking for it, you’ll start noticing the fluency of APIs, as well as languages. One of my favorite fluent APIs is the EasyMock library. When setting up the behavior of a mock object, you can fluently tell it what method to expect and what response to provide, as in this example: Listing 5. Fluent API: EasyMockexpect(alarm.raise()).andReturn(true); expect(alarm.raise()).andThrow(new InvalidStateException()); In the above code fragment, you are coaching the alarm mock. You ask it to return a true in response to the first call to a raise() method. Also, you ask it to throw an exception in response to a subsequent call to the same method. The EasyMock code syntax may confuse you if you haven’t seen something like it before. Once you get the hang of it, however, you will be tempted to write something like it for your own APIs. Another example, from the JSONObject library, shows both fluency and context: Listing 6. Fluency and context in JSONObjectJSONObject json = new JSONObject() .accumulate("key1", "value1") .accumulate("key2", "value2"); The accumulate method acts on a JSONObject, then returns it so that subsequent calls can be chained. In the Guice dependency injection framework, you can set up the binding of interfaces to classes as follows: Listing 7. Fluent dependency injection in Guicebind(Alerter.class) .to(VisualAlerter.class) .in(Scopes.SINGLETON); All of the above examples are from different Java-based APIs, but you can observe two things they have in common. First, they all employ a terse, specialized syntax. Second, they all use method chaining, which means building on what’s returned by a method call. Each of these APIs first establishes a context, and then builds on it. Why context? Conversation is always based on context. If two people enter a conversation without a shared context, they rarely leave it without having created one. Years of shared context allows many married couples to communicate without words. That sounds romantic, but old friends can do it too. For instance, say you see someone dressed like a character from a movie you watched recently with a friend. All you have to do is smile and a nod in the direction of that person, and your friend will probably know that you think the person is like the character from the movie. When two strangers really hit it off in a conversation, shared context is almost always the glue. By the same token, when a conversation simply cannot get off the ground, it may be because shared context is lacking. Context also influences interpretation. For example, you might interpret the words “use a fork” differently depending on context. Someone could be telling a child to use a fork at the dinner table. You could be being instructed to use the fork function to create a child process on a Unix-like system. Or perhaps someone is being coached on a chess move. When it comes to interpretation, context is very important. Like jargon, context reduces the signal-to-noise ratio in communication. In the best cases, it makes communication highly effective. It makes information easier to understand and build on. DSLs similarly benefit a great deal from context — it is shared context that allows us to make our DSLs terse, highly expressive, easy to understand, and easy to work with. Context at work Context was an important element in the method chaining examples I showed you earlier. For instance, you could write the JSONObject example as follows: Listing 8. JSONObject example 1JSONObject json = new JSONObject(); json.accumulate("key1", "value1"); json.accumulate("key2", "value2"); But there is so much unnecessary noise in the repeated use of the json object reference. Instead, the following code builds on the context of the object returned by new. Listing 9. JSONObject example 2JSONObject json = new JSONObject() .accumulate("key1", "value1") .accumulate("key2", "value2"); Context may appear unconventional When following the traditional Java beans convention, you typically write getters, which are query methods that return some value, and setters, which are mutators or modifiers that perform actions but don’t return anything (void methods). In order to realize the context, you break away from the convention of command-query separation. Your mutators, in addition to performing their actions, return this or the self object. It is like bending words and grammar just enough to make a poem rhyme. Languages like JavaScript and Groovy have facilities to build context so you don’t have to break with convention. The with method, used by both Groovy and JavaScript, is one such facility. For example, consider the following example using a Java ArrayList: Listing 10. ArrayList in Javajava.util.ArrayList<String> cart = new java.util.ArrayList<String>(); cart.add("Milk"); cart.add("Juice"); cart.add("Apple"); System.out.printf("My cart has %d items.", cart.size()); Now I’ll re-write the example using the with method: Listing 11. In Groovy context is implicitcart = [] cart.with { add "Milk" add "Juice" add "Apple" println "My cart has $size items." } In the above example, I’ve used the same good old java.util.ArrayList but in Groovy the context is implicit. Calls to add and size are automatically routed to the cart object — Groovy’s with method sets that context. Here’s the same example written using JavaScript: Listing 12. ArrayList is JScart = new java.util.ArrayList() with(cart) { add("Milk") add("Juice") add("Apple") println("My cart has " + size() + " items.") } Creating a fluent, context-aware DSL As a person authoring a class, you are often concerned with state and behavior. As a person using an API, you are often concerned with getting your work done. These two concerns tend to conflict with each other, though they don’t have to. This is where a fluent, context-aware DSL can help a great deal. Suppose you want to create a class for making pizza (a PizzaBuilder class — see Builder Pattern in the Resources section). Following the traditional Java syntax, your API might look like this: Listing 13. PizzaBuilder APIpublic class PizzaBuilder { public void setDough() {} public void setSauce(int amount) {} public void setCheese(int amount, String type) {} public void setToppings(String[] toppings) {} public void bake() {} public Pizza get() { return null; } } (I’ve left the method implementations blank because we are interested in the API or method interfaces, not implementation details.) Here is an example of using the PizzaBuilder: Listing 14. PizzaBuilder makes a Pizzapublic class UsePizzaBuilder { public static void main(String[] args) { PizzaBuilder bldr = new PizzaBuilder(); bldr.setDough(); bldr.setSauce(2); bldr.setCheese(2, "Mozzarella"); bldr.setToppings(new String[] {"Olives", "Tomatoes", "Bell Peppers"}); bldr.bake(); Pizza pizza = bldr.get(); } } The above example has a few problems. First, it has no context, hence the repeated use of the bldr object reference. Second, there is an imposed ordering: the get() method must be the last method to be called. Ordering is a necessary evil in some scenarios, though it would be great if you could avoid it altogether. In cases where you cannot avoid ordering, you can enforce it with tact, by using method chaining to set the return types of methods. Users of your API will then have to follow a certain method order, but doing so will come naturally. A better PizzaBuilder Let’s look at some ways to improve the fluency and context of this API. We’ll start with the Java language, then switch hats and see how to improve the API further using some Groovy constructs. First, in order to improve the API’s fluency, we’ll rename its methods. Instead of traditional setter methods, we can name the methods based on intent. Second, in order to provide context and method chaining, we can rewrite the methods to automatically return the PizzaBuilder a user is working with. Here is the modified PizzaBuilder class (again the implementation of the methods is not shown): Listing 15. Modified PizzaBuilder APIpublic class PizzaBuilder { public PizzaBuilder prepareDough() { return this; } public PizzaBuilder addSauce(int amount) { return this; } public PizzaBuilder addCheese(int amount, String type) { return this; } public PizzaBuilder addToppings(String[] toppings) { return this; } public PizzaBuilder bake() { return this; } public Pizza get() { return null; } } Here’s the new builder in action: Listing 16. A better PizzaBuilder makes a better Pizzapublic class UsePizzaBuilder { public static void main(String[] args) { Pizza pizza = new PizzaBuilder() .prepareDough() .addSauce(2) .addCheese(2, "Mozzarella") .addToppings(new String[] {"Olives", "Tomatoes", "Bell Peppers"}) .bake() .get(); } } The above code is more fluent than the original. We’ve also cut out some noise by getting rid of the repeated instances of the bldr object reference. A user of this API starts with an instance of PizzaBuilder and ends up with an instance of Pizza, which is what he or she was really after. A Groovy PizzaBuilder Let’s see how we can stretch the PizzaBuilder API’s fluency and context even further in Groovy. First, take a look at this modified PizzaBuilder class written in Groovy: Listing 17. PizzaBuilder written in Groovypublic class PizzaBuilder { void prepareDough() { } void addSauce(int amount) { } void addCheese(int amount, String type) { } void addToppings(String[] toppings) { } void bake() { } Pizza get() { return null; } static Pizza make(closure) { PizzaBuilder bldr = new PizzaBuilder(); closure(bldr); return bldr.get(); } } All instance methods except the get method are void methods (meaning they don’t return this). We’ve also added a static method (make()) that takes care of creating an instance of the builder and sending it to the closure. A user of the API can focus on building a pizza instead of creating an instance of PizzaBuilder. Also, the API’s user wouldn’t have to call the get() method — he could even remove it and move its function into the make method. Listing 18 shows the Groovy PizzaBuilder at work. Listing 18. Groovy bakes a pizzaPizza pizza = PizzaBuilder.make { bldr -> bldr.prepareDough() bldr.addSauce(2) bldr.addCheese(2, "Mozzarella") bldr.addToppings("Olives", "Tomatoes", "Bell Peppers") bldr.bake() } Put it in context So far we’ve eliminated the need to call get (and hence the sequence or order of calling), but the API still lacks context. Here’s where Groovy’s with method comes in handy. Listing 19 rewrites make using the with method. Listing 19. Groovy’s with method creates contextstatic Pizza make(closure) { PizzaBuilder bldr = new PizzaBuilder(); bldr.with closure return bldr.get(); } Closures and JavaThe elegance of the Groovy examples comes partly from the use of closures. A process is underway to include closures in the Java language. In the future you will likely be able to use closures as part of your Java language syntax. Do you understand what is going on here? Let’s stop to understand what Groovy’s with() method does. In essence, it makes a clone of the closure given to it, sets the delegate property to the target object (bldr, in this case) and then invokes the cloned closure. So now you might wonder what is the significance of the delegate property of a closure. If a closure can’t find anyone to handle a method, it then, as a last attempt, asks the delegate (if set) if it can handle the method. So, if you simply call a method without any object reference, the delegate gets an opportunity to handle the call. (See Programming Groovy for more about delegates.) Now, the code to make Pizza will look like this: Listing 20. The fastest way to pizza yet!Pizza pizza = PizzaBuilder.make { prepareDough() addSauce(2) addCheese(2, "Mozzarella") addToppings("Olives", "Tomatoes", "Bell Peppers") bake() } That is less noisy — an implicit PizzaBuilder object is still being used, but the API user is not burdened by it. We can increase the API’s fluency by removing the parentheses in the methods that take parameters (for now we’re stuck with parentheses for methods that don’t take parameters). Listing 21. Remove parentheses in methods with parametersPizza pizza = PizzaBuilder.make { prepareDough() addSauce 2 addCheese 2, "Mozzarella" addToppings "Olives", "Tomatoes", "Bell Peppers" bake() } As a last effort toward streamlining the API, it would be great to get rid of the double quotes. If the Cheese type and the toppings names are well known, we can do it easily. In the PizzaBuilder class, we define Mozzarella, Olives, Tomatoes, and Bell_Pepper (and so on) as properties, as shown in Listing 22. Listing 22. One more step toward the fluent APIpublic class PizzaBuilder { def Mozzarella = "Mozzarella" def Olives = "Olives" def Tomatoes = "Tomatoes" def Bell_Peppers = "Bell Peppers" //... Now you can use the PizzaBuilder like so: Listing 23. Spin the pizza!Pizza pizza = PizzaBuilder.make { prepareDough() addSauce 2 addCheese 2, Mozzarella addToppings Olives, Tomatoes, Bell_Peppers bake() } In conclusion In this article you’ve learned about the concepts of fluency and context, and how they apply to domain-specific languages. We started with the simple exercise of seeing how to loop through a collection with less ceremony. You then saw how fluency and context work in different programming languages and APIs, and how they affect usability. Finally, we did an exercise in iteratively improving the user experience of an API. We gradually refined the internal DSL of the PizzaBuilder API, first in Java and then in Groovy, working back and forth to improve both context awareness and fluency. The end result is a Groovy-based PizzaBuilder API that lets the user make a Pizza with great cognitive ease and almost no redundant typing. We’ll return to this example in a future article in this series, seeing how we can further improve the API). But for now … aren’t you feeling hungry for pizza? Dr. Venkat Subramaniam has trained and mentored thousands of software developers in the U.S., Canada, Europe, and Asia. He helps his clients succeed with agile development. He’s author of the book .NET Gotchas (O’Reilly), and coauthor of the 2007 Jolt productivity award-winning book Practices of an Agile Developer (Pragmatic Bookshelf). Venkat is a frequently invited speaker at international conferences. His latest book is Programming Groovy: Dynamic Productivity for the Java Developer (Pragmatic Bookshelf). JavaDevelopment ToolsSoftware Development