Ayende @ Rahien

It's a girl

The magic of boo - Flexible syntax

when I am writing DSL, I keep hitting one pain point. The CLR naming conventions, which are more or less imprinted on my eyelids, are not really conductive to clear reading in a DSL.

Let us take these entities, and see what we get when we try to build a DSL from them:

image

The DSL is for defining business rules, and it looks like this:

when User.IsPreferred and Order.TotalCost > 1000:
	AddDiscountPrecentage  5
	ApplyFreeShipping
when not User.IsPreferred and Order.TotalCost > 1000:
	SuggestUpgradeToPreferred 
	ApplyFreeShipping
when User.IsNotPreferred and Order.TotalCost > 500:
	ApplyFreeShipping

The main problem with this style of writing is that it is visually condense. I can read it pretty much as easily as I read natural English, but anyone who is not a developer really have to make an effort, and even for me, trying to read ruby styled code is easier. Here is how this would look like when using the ruby style conventions:

when User.is_preferred and Order.total_cost > 1000:
    add_discount_precentage 5
    apply_free_shipping
when
not User.is_preferred and Order.total_cost > 1000:
   suggest_upgrade_to_preferred 
    apply_free_shipping
when User.is_not_preferred and Order.total_cost > 500:
   apply_free_shipping

This is much easier to read, in my opinion. The problem is that I consider this extremely ugly.

image

Obviously a different solution is needed...

Wait a minute! Boo has an open compiler. Why not just change the way it handle references? And that is what I did:

///<summary>
/// Allow to use underscore separated names, which will be translated to pascal case names.
/// pascal_case -> PascalCase.
/// All names that contains an underscores will go through this treatment.
///</summary>
/// <example>
/// You can  enable this behavior using the following statement
/// <code>
/// compiler.Parameters.Pipeline
///		.Replace(typeof (ProcessMethodBodiesWithDuckTyping),
/// 				 new ProcessMethodBodiesWithDslNamesAndDuckTyping());
/// </code>
/// </example>
public class ProcessMethodBodiesWithDslNamesAndDuckTyping : ProcessMethodBodiesWithDuckTyping
{
	/// <summary>
	/// Called when we encounter a reference expression
	/// </summary>
	/// <param name="node">The node.</param>
	public override void OnReferenceExpression(ReferenceExpression node)
	{
		if(node.Name.Contains("_"))
			SetNodeNameToPascalCase(node);
		base.OnReferenceExpression(node);
	}

	/// <summary>
	/// Called when we encounters a member reference expression
	/// </summary>
	/// <param name="node">The node.</param>
	public override void OnMemberReferenceExpression(MemberReferenceExpression node)
	{
		if (node.Name.Contains("_"))
			SetNodeNameToPascalCase(node);
		base.OnMemberReferenceExpression(node);
	}

	/// <summary>
	/// Sets the node name to pascal case.
	/// </summary>
	/// <param name="node">The node.</param>
	private static void SetNodeNameToPascalCase(ReferenceExpression node)
	{
		string[] parts = node.Name.Split(new char[] { '_' },StringSplitOptions.RemoveEmptyEntries);
		StringBuilder name = new StringBuilder();
		foreach (var part in parts)
		{
			name.Append(char.ToUpperInvariant(part[0]))
				.Append(part.Substring(1));
		}
		node.Name = name.ToString();
	}
}

I love Boo, with cause.

Comments

grega g
05/27/2008 11:21 AM by
grega g

very nice

i have been reading your blog for some time and i am increasingly interested in boo & dsl topic. But apart from seeing some dsl snippets, i would like an example how this fit into host application. Do you use compiler and compile it to memory? Maybe Interpreter? Or u make dll and reference it in host project.

What about User? This has to be some variable that is supplied from host than consumed by script, changed and than returned. How does one do that?

Ty

grega g

Eyad Salamin
05/27/2008 11:28 AM by
Eyad Salamin

What if you needed to have underscores in one of your properties, won't the compiler crash?

I guess it should try to find the exact match of the reference, and if not found try to replace underscores and "Pascalize" it.

Ayende Rahien
05/27/2008 11:44 AM by
Ayende Rahien

Grega,

To answer your questions would take a book :-)

Luckily I am writing one of the topic.

You can get the information about it here:

http://www.manning.com/affiliate/idevaffiliate.php?id=854_111

Ayende Rahien
05/27/2008 11:45 AM by
Ayende Rahien

Eyad,

Personally, I never use underscores in my properties, so it is not a problem for me.

If you do have this issue, you would need to instruct the compiler in how to deal with this ambiguity

Chris Ortman
05/27/2008 06:07 PM by
Chris Ortman

If I were writing it in ruby I would probably do:

add5percent and apply_freeshipping if user.preferred? and order.totalcost > 1000

Ayende Rahien
05/27/2008 06:14 PM by
Ayende Rahien

It is nice syntax, but I think it put too much information on one line.

Joe Gutierrez
05/28/2008 04:20 AM by
Joe Gutierrez

What would happen if you instantiated, along with the the other business rules:

when User.is_preferred:

apply_free_shipping 

What happens to the other business rules? It would seem that it is possible to have more than one business rule apply.

If the first rule applies, then would this last rule apply. Are you're actions Idempotent?

Ayende Rahien
05/28/2008 07:42 AM by
Ayende Rahien

Yes, all the actions would apply.

Idempotent? I don't think so.

It can be done fairly easily, but I haven't thought about this

Joe Gutierrez
05/29/2008 03:17 AM by
Joe Gutierrez

DSL's are normally declaritive. A challenge I see in this DSL is this:

Order.total_cost > 1000

This is more of a how than a what. If you add some properties to the the order class:

Size.Large, Size.Medium, Size.Small

I think it would be declarative this way. This would then equate to a sentence like this:

when User.is_preferred and Order.Size.Large:

add_discount_precentage 5

apply_free_shipping

Maybe change the action to read:

when User.is_preferred and Order.Size.Large:

apply_preferred_discount

apply_free_shipping

You could add another operation to your actions:

apply_discount 2.5

As a side note, you have a total of 6 combinations of words. The total of original actions that can be created equals 8 different messages. What is up with that?

Another challenge is the Order sizes. If implemented as wriiten in the original rules you may run into some problems like:

when User.is_preferred and Order.Size.Large and not Order.Size.Medium:

This is some pretty exciting stuff! Keep it coming.

Ayende Rahien
05/29/2008 04:03 AM by
Ayende Rahien

Joe,

I am not following about the combination of words.

About Size.Large, I would probably define a bunch of extension methods, so I could do things like:

Order.ApplicableForPrefferedShipping, or, actually:

Order.applicableforpreferred_shipping.

Joe Gutierrez
05/30/2008 02:33 AM by
Joe Gutierrez

You have two states for input User:

IsPreferred

IsNotPreferred

You have three states for Size of order:

total_cost > 1000

total_cost > 500

(whatever isn't covered by the previous states

2*3 = 6 input states

Output states is three

no transactions to all three transaction

2-E3 = 8 states. Due to the number of input states you will not be able to use all 8 states. Of course, some states will be invalid.

What seems to be exciting is that you can create a formal grammar from the set of rules you've implemented.

The real trick that I really haven't seen, but I think that would be really cool is adding a compiling step (annotations?) to the business rules and not let you compile the program if you create a business rule that isn't part of the formal grammar.

The above would be a unit test on possible grammars?

Example:

when User.IsPreferred and Order.TotalCost > 1000:

SuggestUpgradeToPreferred 

ApplyFreeShipping

Business Rule Compile Error: Customer is already preferred, no upgrade required.

Ayende Rahien
05/30/2008 05:17 PM by
Ayende Rahien

Joe,

That is because you are seeing the simplest possible example.

Try to imagine this on a real world system, where you may have tens or hundreds of variables.

In this scenario, trying to define a rigid system breaks.

Comments have been closed on this topic.