Designing With Lambdas - Part II
In my last post I went through a very simple example of applying lambdas to achieve more DRY.
In this installment I'll cheat a little and rehash a previous article I wrote before this blog existed. The article fits rather nicely in this series.
Creating XML with .Net
In .Net two of the most popular ways of creating XML are the System.Xml.XmlDocument
, which implements the XML DOM, and System.Xml.XmlTextWriter
. There's a new interesting way in VB9 using Xml Literals, but it is hardly popular at the time of this writing.
These APIs are obviously old-timers in .Net and were created before lambdas were available. For the sake of comparison, let's see how we would write the following XML document using these two APIs.
<?xml version="1.0" encoding="utf-8"?> <children> <!--Children below...--> <child age="1" referenceNumber="ref-1">child & content #1</child> <child age="2" referenceNumber="ref-2">child & content #2</child> <child age="3" referenceNumber="ref-3">child & content #3</child> <child age="4" referenceNumber="ref-4">child & content #4</child> <child age="5" referenceNumber="ref-5">child & content #5</child> <child age="6" referenceNumber="ref-6">child & content #6</child> <child age="7" referenceNumber="ref-7">child & content #7</child> <child age="8" referenceNumber="ref-8">child & content #8</child> <child age="9" referenceNumber="ref-9">child & content #9</child> </children>
With the good ol' DOM, this document could be produced using something like this.
XmlDocument xml = new XmlDocument(); XmlElement root = xml.CreateElement("children"); xml.AppendChild(root); XmlComment comment = xml.CreateComment("Children below..."); root.AppendChild(comment); for(int i = 1; i < 10; i++) { XmlElement child = xml.CreateElement("child"); child.SetAttribute("age", i.ToString()); child.SetAttribute("referenceNumber", "ref-" + i); child.InnerText = "child & content #" + i; root.AppendChild(child); } string s = xml.OuterXml;
Nothing too dramatic here. But my argument is that the only thing the DOM API has going for it is its ubiquitousness, which is not a minor feat considering how clunky the API is. Look at all those set this and append that. Can you still remember when you were first learning the DOM and never remembering how attributes were set?
Now it's the XmlTextWriter
's turn. Here's the code to write the same XML document.
StringWriter sw = new StringWriter(); XmlTextWriter wr = new XmlTextWriter(sw); wr.WriteStartDocument(); wr.WriteComment("Children below..."); wr.WriteStartElement("children"); for(int i=1; i<10; i++) { wr.WriteStartElement("child"); wr.WriteAttributeString("age", i.ToString()); wr.WriteAttributeString("referenceNumber", "ref-" + i); wr.WriteString("child & content #" + i); wr.WriteEndElement(); } wr.WriteEndElement(); wr.WriteEndDocument(); wr.Flush(); wr.Close(); string s = sw.ToString();
The XmlTextWriter
API is rather efficient but, golly, is it a b!tch to use. No kidding, folks. Miss one of those WriteEndXXXXXX
and you're toast. Good luck in your debugging session.
But enough of bashing our favorite APIs. The point here is just to show a draft of what an API like this could be designed in the era of lambdas.
XmlBuilder - let the lambdas in
What if we could somehow wrap the XmlTextWriter
in a way that we could never forget to close an element? Remember how we wrapped the code in FileUtil.EachLine
in the first installment of this series? We wrote that method in such a way that the file will never be left open by accident. I think we could do the same with the XmlTextWriter
API.
Take a moment to inspect the following code. Put yourself in the shoes of a developer that is trying to write XML for the first time and needs to choose an XML API.
string s = XmlBuilder.Build(xml => { xml.Root(children => { children.Comment("Children below..."); for(int i = 1; i < 10; i++) { children.Element(child => { child["age"] = i.ToString(); child["referenceNumber"] = "ref-" + i; child.AppendText("child & content #" + i); }); } }); });
Did you notice how the code structure maps nicely to the XML document structure? See how there's no way for you to forget one of those AppendChild
calls from the DOM or WriteEndElement
from the XmlTextWriter
?
I particularly like the way the attributes are defined using the indexer syntax. Do you see how I chose to format the lambdas so that they look like C# control blocks? Placing the opening brace of the lambda in the next line created this indented block of code that defines some form of context. The context in this case is "inside this block I'll be building one XML element. When the block ends, the element ends."
You can download the code and play with it. It's only a proof of concept and there's a lot of missing functionality that I hope to implement one day, probably when I decide to use it in some real project.
Explanation of the code
Below you can see an excerpt from the code, showing how the Element()
method was implemented. Let's discuss it.
public virtual void Element(Action<XmlElementBuilder> build) { string name = build.Method.GetParameters()[0].Name; Element(name, new Dictionary<string, string>(), build); } public virtual void Element(string localName, Action<XmlElementBuilder> build) { Element(localName, new Dictionary<string, string>(), build); } public virtual void Element(string localName, IDictionary<string, string> attributes, Action<XmlElementBuilder> build) { XmlElementBuilder child = new XmlElementBuilder(localName, Writer); Writer.WriteStartElement(localName); child._tagStarted = true; foreach(var att in attributes) child[att.Key] = att.Value; build(child);// <-- element content is generated here Writer.WriteEndElement(); _contentAdded = true; }
Looking at the various overload of this method we can see how the lambda comes into play and also at least one more trick. The very first method reflects into the given delegate (the lambda) to determine the name that was used for the single parameter of Action<XmlElementBuilder>
. That's how we did not need to specify the children and child node names. Of course this is not always desirable or possible because the naming rules or XML elements is different than C# identifiers, so the other overloads let us specify the node name.
In the last overload of Element()
is where the real code is. Line #19 Writer.WriteStartElement(localName);
opens the element, line #25 build(child);
invokes the lambda passing a builder instance for what goes inside the element. Line #26 Writer.WriteEndElement();
makes sure we keep synch with the element we started in line #19, by ending it before the method exits.
For easier reference I'm including the code for XmlElementBuilder
and its base class.
public class XmlElementBuilder : XmlBuilderBase { internal XmlElementBuilder(string localName, XmlTextWriter writer) : base(writer) { Name = localName; } public string Name { get; protected set; } public void AppendText(string text) { Writer.WriteString(text); } }
public abstract class XmlBuilderBase { protected XmlBuilderBase(XmlTextWriter writer) { Writer = writer; } internal XmlTextWriter Writer { get; set; } private bool _contentAdded = false; private bool _tagStarted = false; public virtual void Comment(string comment) { Writer.WriteComment(comment); _contentAdded = true; } public virtual void Element(Action<XmlElementBuilder> build) { string name = build.Method.GetParameters()[0].Name; Element(name, new Dictionary<string, string>(), build); } public virtual void Element(string localName, Action<XmlElementBuilder> build) { Element(localName, new Dictionary<string, string>(), build); } public virtual void Element(string localName, IDictionary<string, string> attributes, Action<XmlElementBuilder> build) { XmlElementBuilder child = new XmlElementBuilder(localName, Writer); Writer.WriteStartElement(localName); child._tagStarted = true; foreach(var att in attributes) child[att.Key] = att.Value; build(child);// <-- element content is generated here Writer.WriteEndElement(); _contentAdded = true; } Dictionary<string, string> _attributes = new Dictionary<string, string>(); public string this[string attributeName] { get { if(_attributes.ContainsKey(attributeName)) return _attributes[attributeName]; return null; } set { if(_contentAdded) throw new InvalidOperationException( "Cannot add attributes after" + " content has been added to the element."); _attributes[attributeName] = value; if(_tagStarted) Writer.WriteAttributeString(attributeName, value); } } }
I realize the code I'm providing here is not a complete solution for all XML creation needs, but that's also not the point of this series. The idea here is to explore ways to incorporate lambdas in the API. When you think about it, this design has been possible all along via delegates since .Net 1.0. Anonymous delegates made this a much, much better. But only with the expressive lambda syntax we are seeing an explosion of this type of delegate usage.