Behind the scenes of the planning DSL

Published 09 July 07 10:54 PM | andersnoras 

Last week I blogged about an embedded DSL for creating different calendar components like events and to do-tasks. For this post I'll let you in on some of the "secrets" of embedded DSL development in C#. Before we get started, let me refresh your memory with an example of how the DSL can be used:

ToDoComponent planningTask =
    Plan.ToDo("Plan project X").
        StartingNow.
        MustBeCompletedBy("2007.08.17").
        ClassifyAs("Public");
planningTask.Save();

EventComponent planningMeeting =
    Plan.Event("Project planning meeting").
        RelatedTo(planningTask).
        WithPriority(1).
        At("Head office").
        OrganizedBy("jane@megacorp.com", "Jane Doe").
        StartingAt("12:00").Lasting(45).Minutes.
        Attendants(
            "peter@megacorp.com",
            "paul@megacorp.com",
            "mary@contractor.com").AreRequired.
        Attendant("john@megacorp.com").IsOptional.
        Resource("Projector").IsRequired.
        ClassifyAs("Public").
        CategorizeAs("Businees", "Development").
        Recurring.Until(2008).EverySingle.Week.On(Day.Thursday).
        Except.Each.Year.In(Month.July | Month.August);

planningMeeting.SendInvitations();

Writing DSLs is a little different from the regular object oriented programming style. You might have noticed that the Plan class has a verb for its name rather than the usual noun. This allows us to have a natural starting point for writing out the "sentence" explaining our intention.

public class Plan
{
    private Plan() {}
    public static ToDoDescriptor ToDo(string description)
    {
        ToDoDescriptor descriptor=new ToDoDescriptor();
        descriptor.Description= description;
        return descriptor;
    }
    public static EventDescriptor Event(string description)
    {
        EventDescriptor descriptor=new EventDescriptor();
        descriptor.Description = description;
        return descriptor;
    }
}

Rather than returning an instance of the type we want to create, the operations on the Plan class return descriptors which we will use to collect the information needed to create these types. These descriptor types is the backbone of the DSL. The DSL is basically a fluent interface. With some exceptions, every method or property in the fluent interface returns an instance of the class it is defined on allowing operations to be chained together. Take a look at the following excerpt from the EventDescriptor class.

public class EventDescriptor
{
    // Lots of stuff eluded for brevity...
    public EventDescriptor StartingAt(string time)
    {
        return StartingAt(Convert.ToDateTime(time));
    }
    public EventDescriptor StartingAt(DateTime time)
    {
        this.startTime = time;
        return this;
    }        
    public EventDescriptor ShownAsBusy
    {
        get
        {
            timeTransparency = TimeTransparency.Opaque;
            return this;
        }
    }
    public EventDescriptor ShownAsFreeTime
    {
        get
        {
            timeTransparency = TimeTransparency.Transparent;
            return this;
        }
    } 
}

Notice that every property and method returns "this" to the caller. Another abnormality is that the getters actually change the state of the EventDescriptor class. This allows our language to be more natural than if we'd followed the common design guidelines.
Another takeaway is that there are convenient overloads for some of the methods. For instance the StartingAt method accepts a string in addition to an actual DateTime make the client code easier to read.

Plan.Event("Meeting").
    StartingAt("2007.07.10 12:00");
//... is easier to read than ...
Plan.Event("Meeting").
    StartingAt(new DateTime(
        2007,07,10,12,00,00
    ));

A common way to write "regular" object oriented code is to accept the required arguments in the constructor and allow the user to set any additional properties afterwards through the setters. For instance the EventComponent class (which is written adhering to the rules of the OO schoolbook) allows this.

EventComponent meeting=new EventComponent("Meeting");
meeting.Organizer=new Organizer("jane@megacorp.com");
meeting.Organizer.CommonName="Jane Doe";

In the DSL you'll use the OrganizedBy operation to specify who the meeting organizer is, and since there is a lot of different optional properties to set on the Organizer class the OrganizedBy method has quite a few overloads to allow the user to specify these.

public EventDescriptor OrganizedBy(CalAddress organizer)
{
    this.organizer = new Organizer(organizer);
    Attendee chair = new Attendee(organizer);
    chair.Role = ParticipationRole.Chair;
    attendees.Add(chair);
    return this;
}
public EventDescriptor OrganizedBy(CalAddress organizer, string organizerName)
{
    OrganizedBy(organizer);
    this.organizer.CommonName = new CommonName(organizerName);
    return this;
}
public EventDescriptor OrganizedBy(CalAddress organizer, string organizerName, string directory)
{
    OrganizedBy(organizer,organizerName);
    this.organizer.Directory = new Directory(directory);
    return this;
}

Another thing you should notice is that the topmost overload also adds the organizer as an attendee to to the meeting. This saves the user from typing at the same time as it improves the quality of the event component produced in the end. This shows how we can use the DSL interface to provide an more expressive language on top of the underlying API.

In the example shown at the top of this post, the DSL is really a special case of the Factory design pattern. We therefore need to terminate any "sentence" with an operation that returns an EventComponent instance. The common way of doing this is to have an interface like this:

MyObject o=The.Last().Operation().Returns.The.Instance();

Where the Instance() method would return an instance of MyObject. With smaller, well defined DSLs this is usually not a problem because you can live with a strict grammar. E.g. every sentence consists of the same words.
However, in an advanced scenario like our event planning DSL we need less strict grammar to cover all the use cases. For instance you don't have to set a priority, location or similar for an event, neither do you have to make it recursive and even if it is recursive, it doesn't need to have exceptions. This implies that all of the following "sentences" should be allowed.

EventComponent thing;
thing=
   Plan.Event("Dinner").
       At("Dino's Diner").
       OrganizedBy("jane@megacorp.com", "Jane Doe").
       StartingAt("19:00").EndingAt("21:00").
       Attendant("john@megacorp.com").IsOptional.
       ClassifyAs(Classfication.Private);

thing=
    Plan.Event("Time to think").
        WithPriority(Priority.Cua.A3).
        ShownAsFreeTime.
        StartingAt("12:00").Lasting(45).Minutes.
        Resouce("Clear mind").IsRequired.
        CategorizeAs("Development").
        Recurring.UntilForver.EveryOther.Week.
        Except.Once.EverySingle.Year.In(Month.December);

thing =
    Plan.Event("Summer vacation").
        StartingAt("2007.08.06").Lasting(3).Weeks.
        ClassifyAs("Private").
        Recurring.Times(4).EverySingle.Year;

Even if there is a clear structure of the grammar, there are no final terminating keyword at the end of each sentence, so ClassifyAs(), In() and Year must all return a valid EventComponent. However, they can also have additional keywords following them. For instance the ClassifyAs() operation can be followed by any of these keywords:

  • At()
  • Attendant()
  • CategorizeAs()
  • ClassifyAs()
  • EndingAt()
  • Lasting()
  • OrganizedBy()
  • RelatedTo()
  • Resource()
  • Resources()
  • StartingAt()
  • WithPriority()
  • Recurring
  • ShownAsBusy
  • ShownAsFreeTime

In other words the method returns an EventDescriptor instance. To allow ClassifyAs (or any of the other keywords in the above list) to be a terminating keyword we create an implicit cast operator on the EventDescriptor type allowing to to be casted to an EventComponent.

public static implicit operator EventComponent(EventDescriptor descriptor)
{
    return CreateEvent(descriptor);
}

internal static EventComponent CreateEvent(EventDescriptor descriptor)
{
    EventComponent component = new EventComponent();
    component.Attendees = descriptor.attendees;
    component.Categories = descriptor.categories;
    component.Classfication = descriptor.classification;
    component.Created = DateTime.Now;
    component.Description = descriptor.description;
    component.TimeStamp = DateTime.Now;
    component.StartTime = descriptor.startTime;
    component.ExceptionRule = descriptor.exceptionRule;
    component.GeographicalPosition = descriptor.geographicalPosition;
    component.LastMod = DateTime.Now;
    component.Location = descriptor.location;
    component.Organizer = descriptor.organizer;
    component.Priority = descriptor.priority;
    component.RecurrenceRule = descriptor.RecurenceRule;
    component.Summary = descriptor.summary;
    component.Transparency = descriptor.timeTransparency;
    return component;
}

The ability to create custom casting operators is a huge benefit when you develop fluent interfaces with C#.

Astute readers might wonder why the creation of the actual EventComponent is delegated to a separate method. Before we get to that, we need to look at some other parts of the language.
Consider the following snippet:

Plan.Event("Summer vacation").
    StartingAt("2007.08.06").Lasting(3);

If every keyword had returned an EventDescriptor, this would have been a legal sentence in the language. How would we know how long the event lasted? It lasts for three, but what is three? To force the user to specify this the Lasting() operation returns a DurationDescriptor rather than an EventDescriptor. The DurationDescritor doesn't have an implicit cast operator that allows it to be casted to an EventComponent, so the snippet above won't compile.

public class DurationDescriptor
{
    private EventDescriptor eventDescriptor;
    private int units;
    internal DurationDescriptor(EventDescriptor descriptor)
    {
        this.eventDescriptor = descriptor;
    }
    internal int Units
    {
        get { return units; }
        set { units = value; }
    }

    public EventDescriptor Seconds
    {
        get
        {
            eventDescriptor.Length = new TimeSpan(0, 0, 0, units);
            return eventDescriptor;
        }
    }

    public EventDescriptor Minutes
    {
        get
        {
            eventDescriptor.Length = new TimeSpan(0, 0, units, 0);
            return eventDescriptor;
        }
    }

    public EventDescriptor Hours
    {
        get
        {
            eventDescriptor.Length = new TimeSpan(0, units, 0, 0);
            return eventDescriptor;
        }
    }
    // Other properties like Days and Weeks go here...
}

As you can see from the snippet above, well get our EventDescriptor back when we choose one of the properties on the DurationDecriptor. Another difference from plain old OO is that the getters on the DurationDescriptor change the state of the event descriptor. Again this diversion from the rules of OO-design allows us to make our grammar flow nicely.
Creating branches within our grammar using different descriptor objects like this is a good way of restricting the choices the user has, and it helps keep the language consistent. The downside is that we have to write many different descriptor classes when the scenario gets more complex than with the DurationDescriptor. A total of twenty six different descriptor classes are used to control the grammar and flow of the recurrence and exception DSL.
You might have noticed that a sentence could end with a partial recurrence or exception rule, this implies that some of these descriptors can be casted to EventComponents as well. The EventDescriptor exposed the CreateEvent so that any descriptor can create an EventComponent from its current description when this is needed.

Martin Fowler pays some attention to the price of fluency in his article on fluent interfaces. It is more difficult to write a fluent interface than a plain old API, and I agree with Martin that it takes quite a lot of though to get the grammar right. I've found that it is best to start with a test case and just write the "sentences" before starting to implement the actual language. Not only does this help you to come up with a good language, it also helps create all of the classes, methods and properties that are needed - at least as long as you're using ReSharper. An believe me, there will be a lot of code when you're done. I'm almost there with this DSL, and at the time of writing is consists of 58 classes not including the API and tests.

Filed under: , ,

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

# Jeremy D. Miller -- The Shade Tree Developer said on July 10, 2007 2:50 PM:

More C# DSL goodness from Anders: Behind the scenes of the planning DSL

# the ‘bee log / Ordered Fluency said on July 12, 2007 1:08 PM:

PingBack from http://laribee.com/blog/2007/07/12/ordered-fluency/

# TrackBack said on July 13, 2007 2:40 AM:
# Passion is like genius; a miracle. :: Designing DSL said on July 14, 2007 3:15 AM:

PingBack from http://mkseo.pe.kr/blog/?p=1703

# Adde said on July 14, 2007 2:02 PM:

Nice, but you're essentially throwing static typing and interfaces out the window which means you're back to checking each required field manually.

Why not go all the way and either use a language like Haskell where stuff like this can be expressed without sacrificing static typing or use a dynamically typed language?

I guess it could be a viable alternative if all the fields are optional.

# Joe Ocampo said on July 14, 2007 11:45 PM:

Very nice.  Is there anyway we can download the code?

# andersnoras said on July 15, 2007 11:59 AM:

@Adde;

I'm not sure if I follow you here. Just like any other C# code, the interface is strongly typed. There are some weaker typing (like the ability to use as string for a date) that require some parsing, but this is just there for convenience I've also done overloads that are strongly typed. As for setting values on the produced object you have the possibility of overwriting the same value within a sentence. For instance if you specify the Organizer twice. Other duplicates, such as two Attendee(string).IsRequired statements, both are added to the underlying object.

Most of the fields are optional and there are good defaults provided for those who aren't. You always have the option to throw a runtime exception if an incomplete object is created, but I can agree that is is a poor replacement for a strongly typed constructor.

Typing is also used for language flow control, for instance the intellisense for the EventDescriptor class lists the methods and properties in the bullet list (above), while the various  recurrence and exception branches have their own limited vocabulary. For instance the intellisense for the "Recur" keyword lists the following possibilities Once, Twice, Times() and Until().

@Joe;

Thanks - very much along the lines of what you're up to with NUnit.Behave which also looks very good.

Due to some licensing issues, I cannot make the code publicly available at the time being. I hope to sort this out soon, and I'll post everything then.

# Anders Norås' Blog said on July 15, 2007 3:11 PM:

My latest posts on DSLs caused a bit of a stir around the blogosphere. Some of of the more constructive

# Console.Write(this.Opinion) said on July 16, 2007 6:17 AM:

Resumo da semana - 06/07/07

# Ayende @ Rahien said on July 17, 2007 4:37 PM:

Fluent Interfaces

# Insane World said on July 18, 2007 11:34 PM:

the

# FictionSoft Blog » Fluent Interfaces - Mini DSLs said on July 30, 2007 5:07 PM:

PingBack from http://www.fictionsoft.com/2007/07/fluent-interfaces-mini-dsls/

# Software Architect’s Site » Blog Archive » DslReadings said on September 18, 2007 4:38 AM:

PingBack from http://projects.akela.ro/portal/architectsblog/?p=122

# Anders Norås' Blog said on November 6, 2007 2:20 PM:

Casper wrote an interesting comment to my screen cast about LINQ and Quaere auto completion which deserves

# Anders Norås' Blog said on January 30, 2008 12:52 PM:

Today it is exactly one year since I pick up on blogging after a long break. To celebrate, I’ll

# dave^2=-1 said on February 1, 2008 2:51 AM:

Anders has a fantastic post about creating DSLs using C# . He covers the normal design rules you need

# Event and Conference Planning said on February 16, 2008 3:24 AM:

Much of the info that you may learn of dealing with this topic are very knowledgeable, while many are not.

# Business Planning A Successful Sale Event said on February 16, 2008 7:09 PM:

Sometimes, you\'ll get bogged down by the colossal sum of Corporate Event Planning UK resources out there.

# Creative Event Planning said on February 16, 2008 10:44 PM:

Occasionally, you\'ll get stunned by the enormous sum of Event Coordinating Planning Careers resources handy.

# Event Planning Denver said on February 18, 2008 10:42 AM:

As you are looking for Las Vegas Event Planning info and stories, be sure to use all of the resources at your disposal.

# Event Job Planning said on February 18, 2008 4:02 PM:

It is legitimate that obtaining proven info on this subject can be time consuming.

# Budget Event Planning said on February 19, 2008 7:19 PM:

Having a way to never-ending websites in reference to this is prime.

# Livonia Company Event Planning said on February 20, 2008 1:51 AM:

Many web pages you track down have everything you are looking for.

# Michigan Youth Group Event Planning said on February 20, 2008 2:09 AM:

While cyberspace continues to pile up Business Event Planning guidance, we\'ll strive to deliver them to you.

# A Main Event Event Planning said on February 20, 2008 3:28 PM:

A great amount of the sites that you will find dealing with this matter are very knowledgeable, while many are not.

# Festivals Event Planning said on February 23, 2008 4:30 PM:

Tracking down the number one Event Planning Business web pages is not easy.

# Lawrence said on February 25, 2008 9:44 AM:

This is an excellent article about DSL. However,

satellite DSL should be considered as an alternative, particulary in rural areas without regular DSL or cable internet access.

http://www.1-satellite-tv-facts.com/Satellite-DSL.html

# Paul said on May 21, 2008 3:40 PM:

More information about high speed internet providers are at

http://www.1-satellite-tv-facts.com/T1-Internet-Service.html

http://www.1-satellite-tv-facts.com/Satellite-Internet.html

http://www.1-satellite-tv-facts.com/VoIP.html

http://www.1-satellite-tv-facts.com/Phone-Systems.html

# Mo Khan: My Blog! said on July 6, 2008 7:45 PM:

Leave a Comment

(required) 
(optional)
(required) 
Enter the code you see below