Is it a bird? Is it a plane? It's a DSL!
My latest posts on DSLs caused a bit of a stir around the blogosphere. Some of of the more constructive debates have been on wether a fluent interface is just a fluent interface or if it can be labeled as a DSL. I have a consultant's answer to this; it depends. I my opinion my posts on the planning DSL concerns a DSL, even if it technically is just a fluent interface. Scott Bellware has an argument that is hard to disagree with - "Whether fluent interface is a form of DSL or not, it's obviously a form of fluent interface.". Scott's contributions to the debate are much concerned about the confusion one might introduce by mixing the terms internal DSL and fluent interface. The fluent interface is the pattern in use, a DSL is more of a design style and it is here my key motive for calling this a DSL lies. In addition to using a fluent interface to allow users to express their intentions in a form that is close to a well formed (albeit long and tedious) English sentence, I also used a number of other design techniques to make the fluent interface more expressive. The most important of these is the humane interface. A humane interface differs from what we are used to within the .NET and Java worlds because they are larger and are stuffed with convenience methods that aren't technically needed to get the job done. These interfaces have the additional verbosity to make it more pleasant to work with. Martin Fowler's article on the topic is a great read.
Still from a patterns point of view, the pattern used in the example in question is the fluent interface pattern. Whether it is written in C# or any other language this is one of the central patterns being applied within internal DSLs. Others include variations on the builder and factory patterns, which I will return to later in this post. To give people a better understanding of what the are doing I think we who are promoting DSLs have a great responsibility for making people see the connections to other OOP techniques. I appreciate Scott's effort to make this clear, and even if I've never met him - I'll gladly shake your hand if I ever do. :-)
I fully agree that more dynamic languages like Ruby, Groovy or Boo lend themselves better to internal DSL development than static languages like C# or Java, but unfortunately many developers do not have the option to choose these because of corporate policies or other limitations. I stated my venture into writing DSLs with statically typed languages to help people think inside a bigger box to improve their code and make it more user friendly by applying some of the techniques that have made dynamic language so popular, so fast.
When it comes to developing DSLs the ability to implement methods at runtime, for instance via Ruby's magical method_missing, is a huge advantage with dynamic languages. If C# had sported the recurrence branch of the language could had pure interface like;
Recur.Once, Recur.Twice and Recur.ForthyFiveTimes rather than a Times() method in combination with "once" and "twice" properties for convenience.
Apart from the obvious "dynamicity" of dynamic languages, many of these also have lax rules on whether you have to use parenthesis or not. This can often be leveraged to make a DSL more readable, but you'll never get a true English sentence with an embedded DSL - it will be interrupted by language specific punctuation. Consider this snippet from Specter:
specify people.Must.SomeSatisfy({ p | return p.Name == "andrew" })
This shows a common pattern seen in internal DSLs. Joe Oscampo's NUnit.Behave, which is a pure C# DSL, uses a fluent interface to achieve something similar:
Scenario("savings account is in credit");
Given("my savings account balance is", 100,
delegate(int accountBallance)
{
savings = new Account(accountBallance);
});
Given("my cash account balance is", 10,
delegate(int accountBallance)
{
cash = new Account(accountBallance);
});
When("I transfer to cash account", 20,
delegate(int transferAmount)
{
savings.TransferTo(cash, transferAmount);
});
Then("my savings account balance should be", 80,
delegate(int expectedBallance)
{
Assert.AreEqual(expectedBallance, savings.Ballance);
});
Then("my cash account balance should be", 30,
delegate(int expectedBallance)
{
Assert.AreEqual(expectedBallance, cash.Ballance);
});
Both of these uses closures, but the C# syntax is much more verbose than its Boo equivalent. If you're to do the same thing with Java its even worse.
The point is that any internal DSL will be obscured by the limitations of the language. If you want a clean, reads like plain English DSL, you'll have to resort to an external DSL. If you writing a parser is an option you can take this very far. Consider this example from the Daily WTF's recent programming contest.
Let's assume that tab equals 8 spaces
Let's draw characters randomly
Let's allow user to make mistakes
Now we'll define numbers
Zero is
a large circle
One is
either
a long vertical line
or
a long vertical line
a very small slash line
top of first joins top of second
Two is
either
a part of circle 12 to 6
a long horizontal line
6 of first joins left of second
or
a part of circle 9 to 5
a slash line
a long horizontal line
5 of first joins top of second
bottom of second joins left of third
When speaking of external versus internal DSLs, one argument that came up was that internal DSLs aren't true DSLs because they don't introduce any new keywords into a language and hence its just an API style. I believe that this is only partially true, and again it depends on the capabilities of the hosting language. C# is very limited in this sense, but Java 1.5+ and Visual Basic have their static imports which can be exploited to do this. I've written about how one can achieve this in Java in an earlier post, so let us redo this little DSL in Visual Basic. First we'll need a class that defines the different keywords of the language. Since we'll be leveraging Visual Basic's static imports feature this needs to be a class with shared (which is VB-speak for static) methods.
Public Class XmlBuilder
Private Shared Property doc As XmlDocument
Get
Dim lds As LocalDataStoreSlot = Thread.GetNamedDataSlot("doc")
Dim instance As XmlDocument
instance = CType(Thread.GetData(lds),XmlDocument)
If (instance = Nothing) Then
instance = New XmlDocument
Thread.SetData(lds, instance)
End If
Return instance
End Get
Set
Dim lds As LocalDataStoreSlot = Thread.GetNamedDataSlot("doc")
Thread.SetData(lds, value)
End Set
End Property
Public Overloads Shared Function Document() As XmlDocument
Dim result As XmlDocument = CType(doc.CloneNode(true),XmlDocument)
doc = Nothing
Return result
End Function
Public Overloads Shared Function Document(ByVal rootNode As XmlNode) As XmlDocument
doc.AppendChild(rootNode)
Return Document
End Function
Public Overloads Shared Function Element(ByVal name As String, ParamArray ByVal childNodes() As XmlNode) As XmlElement
Dim elm As XmlElement = doc.CreateElement(name)
For Each node As XmlNode In childNodes
If (node = XmlAttribute) Then
elm.SetAttributeNode(CType(node,XmlAttribute))
Else
elm.AppendChild(node)
End If
Next
Return elm
End Function
Public Overloads Shared Function Element(ByVal name As String, ByVal value As Object) As XmlElement
If (value = XmlNode) Then
Return Element(name, CType(value,XmlNode))
End If
Dim elm As XmlElement = doc.CreateElement(name)
elm.InnerText = Convert.ToString(value, CultureInfo.InvariantCulture)
Return elm
End Function
Public Shared Function Attribute(ByVal name As String, ByVal value As Object) As XmlAttribute
Dim attr As XmlAttribute = doc.CreateAttribute(name)
attr.InnerText = Convert.ToString(value, CultureInfo.InvariantCulture)
Return attr
End Function
Public Shared Function Text(ByVal value As Object) As XmlText
Dim elm As XmlText = doc.CreateTextNode(Convert.ToString(value, CultureInfo.InvariantCulture))
Return elm
End Function
Public Shared Function Comment(ByVal comment As String) As XmlComment
Return doc.CreateComment(comment)
End Function
End Class
This is a straight port from my Java example. Read this to get an explanation of what the code does. The above class is tailored to be used as keywords within a client application, but the trick to achieve this lies in the client code. The first thing we'll need to do is import the static methods of this class.
Imports XmlBuilder
After doing this, we can use the different methods as "keywords" within our client application to build an XML document.
Dim doc As XmlDocument
doc = Document( _
Element("Employees", _
Element("Employee"), _
Comment("Highly valued employee..."), _
Element("Id", 42), _
Element("Name", "Arthur Dent"), _
Element("ContactInfo", _
Element("PhoneNumber", "555-5555"), _
Element("Address", "Upda road 42"), _
Element("Country", "England")), _
Element("Employee", _
Attribute("toBeFired", True), _
Element("Id", 3.14), _
Element("Name", "Lay C. Slobb"), _
Element("SpecialChars", "< & > @"))))
This isn't as domain specific as the upcoming XML literal features of Visual Basic 9 or Java 1.7, but it is much more convenient than mucking about with the DOM API to create the XmlDocument instance.
Even if this never has been the intention for static imports, they can be "misused" to introduce new "keywords" into a language. Combining static imports and fluent interfaces in internal DSLs introduce some interesting opportunities. LINQ is really syntactic sugar over a clever API, and the query language is a brilliant example of compiler macro DSL and is in that sense similar how Boo and Groovy macros work. Still, the value of LINQ is the ability to have a common query language for anything that implements IEnumerable or IQueryable. I've done quite a bit of work to do something similar with Java, and since Java doesn't have any intrinsic support for compiler macros, I've resorted to combining static imports and fluent interfaces to build my DSL. Below is an example of a client using this experimental DSL to query a collection of domain objects.
Product[] products = Product.getAllProducts();
Object[] expensiveInStockProducts = (Object[])
from(products).as("p").
where(
and(
gt("p.unitsInStock", 0),
gt("p.unitPrice", 3)
)).select("p");
This examples uses all of the techniques discussed in this posting, and if you look aside from the parenthesis, dots and semicolons the lines are blurred between what is Java and what is an API. Because of this I believe that this more than just a couple of factories, some builders and a fluent interface - it's a DSL.