Creating DSLs in Java, Part 3: Internal and external DSLs

Parse and refine an external DSL with openArchitectureWare's Xtext

You understand the basics of domain-specific languages and now you're ready to begin creating and refining them for your projects. In this third article in his series Venkat Subramaniam shows you how to create both internal and external DSLs using Java code. He explains the difference between the two types of DSL and why Java is a better choice for creating one type than the other. He also introduces the various options for parsing external DSLs -- as plain text, as XML, or using an industrial-strength language recognition tool such as ANTLR or openArchitectureWare.

Using semicolons or a pair of parentheses in code is second nature to those of us who are accustomed to C-derived languages. Our pinkies place semicolons involuntarily, and our eyes easily gloss over them. In DSLs, however, the accumulation of parentheses and semicolons is just so much noise, having a negative affect on fluency.

One way to decrease the noise level in your DSLs is trade in your semicolons for dots (.) and using method chaining. In addition to making your code more fluent, method chaining also helps with context, by removing the need to repeat an object reference. The JSON example in Part 2 of this series illustrates the value of method chaining. (See "Creating DSLs in Java, Part 2," Listing 9). You can also use the static import feature in Java 5 to eliminate object or class references. The EasyMock example in "Creating DSLs in Java, Part 2," Listing 5, illustrates this.

Internal vs external DSLs

Internal DSLs ride on a host language, so an internal DSL's syntax is both influenced and restricted by the host language. External DSLs can be built from the ground up but doing so requires mental muscle, as well as a very good parser.

In this article we'll see what happens when we apply these techniques to create a fluent, context-aware interface using Java code. The first DSL we'll build is an internal DSL written using the Java language syntax.

Internal DSLs in Java

Let's assume you want to send out some emails. You need a class that will simply allow you to specify the from, to, subject, and message data. You don't want to bother about object state or even create objects. You can start out with a traditional Java API. Study the API first, then we'll refactor it for more fluency. (Only the interface is shown because the implementation details aren't relevant to the example.)

Listing 1. Mailer: A traditional Java API

package dsl.example;
public class Mailer
{
  public void from(String fromAddress) {}
  public void to(String toAddress) {}
  public void subject(String theSubject) {}
  public void message(String body) {}
  public void send() {}
}

To use this class, you would write something like this:

Listing 2. Creating a new Mailer and sending mail

Mailer mailer = new Mailer();
mailer.from("build@example.com");
mailer.to("example@example.com");
mailer.subject("build notification");
mailer.message("some details about build status");
mailer.send();

A more fluent Mailer

So far there are still quite a few steps between you and sending mail. That may be fine for a programmer, but not for a DSL user. What can we do to make this API fluent, and more like a DSL?

First, each of the methods of Mailer, except the send() method, can return itself, so we can chain the calls, as shown in Listing 3.

Listing 3. Method chaining in the Mailer API

package dsl.example;
public class Mailer
{
  public Mailer from(String fromAddress) { return this; }
  public Mailer to(String toAddress) { return this; }
  public Mailer subject(String theSubject) { return this; }
  public Mailer message(String body) { return this; }
  public void send() {}
}

You would use this modified version as shown in Listing 4.

Listing 4. Sending mail with a new Mailer

new Mailer()
  .from("build@example.com")
  .to("example@example.com")
  .subject("build notification")
  .message("some details about build status")
  .send();

That's a notch better; however, it would be nice to eliminate the use of new. After all, as a user of this DSL your main interest is in sending out emails, not in creating objects. We want to eliminate as much verbosity from the DSL as possible.

Listing 5. Mailer DSL, third iteration

package dsl.example;
public class Mailer
{
  public static Mailer mail() 
  { 
    return new Mailer();
  }
  public Mailer from(String fromAddress) { return this; }
  public Mailer to(String toAddress) { return this; }
  public Mailer subject(String theSubject) { return this; }
  public Mailer message(String body) { return this; }
  public void send() {}
}

Here's how the above DSL works:

Listing 6. Mailer sends mail!

Mailer.mail()
  .from("build@example.com")
  .to("example@example.com")
  .subject("build notification")
  .message("some details about build status")
  .send();

If you're using Java 5, you can perform a static import of the mail method, like so:

import static dsl.example.Mailer.mail;

Once you add the static import, you don't need the class reference to call the mail method (assuming there is no collision with any other mail method). The code distills to the following:

Listing 7. Mailer: A fluent, context aware DSL

mail()
  .from("build@example.com")
  .to("example@example.com")
  .subject("build notification")
  .message("some details about build status")
  .send();

The three-times refactored DSL in Listing 7 shows exactly how much fluency you can realize using Java code today. You are stuck with the parentheses and the ending semicolon. You can go a lot further with dynamic languages like JRuby and Groovy -- and not (as you might think) because these languages are typeless or have relaxed typing. On the contrary, it is the relaxed syntax of these languages that makes them more fluent. The syntax of dynamic and more modern languages requires less ceremony. I'll show you what I mean by that in the final article in this series.

1 2 3 4 5 Page 1
Page 1 of 5
InfoWorld Technology of the Year Awards 2023. Now open for entries!