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:
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.
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
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
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.
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
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
If I were writing it in ruby I would probably do:
add_5_percent and apply_free_shipping if user.preferred? and order.total_cost > 1000
It is nice syntax, but I think it put too much information on one line.
What would happen if you instantiated, along with the the other business rules:
when User.is_preferred:
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?
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
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:
Maybe change the action to read:
when User.is_preferred and Order.Size.Large:
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.
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.applicable_for_preferred_shipping.
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:
Business Rule Compile Error: Customer is already preferred, no upgrade required.
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.
Comment preview