I am going to start writing this soon. Any suggestions?
As a bait, I am going to apply the techniques that I intend to describe to Binsor, which should give you incentive to say what you really want from a DSL documentation... :-)
I am going to start writing this soon. Any suggestions?
As a bait, I am going to apply the techniques that I intend to describe to Binsor, which should give you incentive to say what you really want from a DSL documentation... :-)
A few hours ago I finished writing Chapter 10. Looking at the time on the calendar, it didn't take too long, about a month. Looking at it from the point of view of effort involved, I feel about as tired as if it took three years.
Other chapters took longer, but didn't take out quite as much effort. Not sure what this means... but I am happy I am done with it (for a given value of done, I will still need to review and read it at least three dozen times).
Probably one of the hardest challenges that I am facing with writing the book is to know what to say and what to leave unsaid.
Phrasing it another way, it is choosing at what level to talk to the reader. On the one hand, I really want the reader to be able to make immediate use of the concepts that I am talking about, which drive me to do more practical demonstrations, code samples and covering more common situations. On the other hand, those take up a lot of room, and they tend to be boring if you don't need exactly what you need right this moment.
High level concepts, open ended possibilities and assuming a bit about the reader knowledge level makes for a book that is much more narrowly focused, and I think that it more valuable. However, it also tend to leave readers unsatisfied, because not everything is explained.
Currently I am writing a UI focused chapter, and to get a good experience from the UI you need to invest a lot of time. Metric tons of it. I am trying to chart the way and show how this can be done, but without getting mired in all the actual minute details.
This is a tough balancing act, and I am not sure if I am succeeding.
Going back to the theme of visually editing a DSL, we have something like this:
The backend to this cutey is pattern recognition with custom UI on top. You have to teach it about each pattern that you use with it, but you can get pretty smart with how it works while using a fairly brute force (and simple) techniques.
Thoughts?
One of the more annoying problems with building rules that are also code is that you have to deal with code related issues. One of the more common ones is NullReferenceException.
For example, let us say that we have the following rule:
when Order.Amount > 10 and Customer.IsPreferred: ApplyDiscount 5.precent
We also support a mode in which a customer can create an order without actually registering on the site (anonymous checkout).
In this scenario, the Customer property is null. We can rewrite the rule to look like this:
when Order.Amount > 10 and Customer is not null and Customer.IsPreferred:
ApplyDiscount 5.precent
But I think that this is extremely ugly. We can also decide to return a default instance of the customer when it is not there, but here I want to show you another way to handle this. We define the rule as invalid when Customer is not there, so it should not be run. The question is how we can know that.
The dirty way is to do something like this:
var referencesCustomer = File.ReadAllText(ruleName).Contains("Customer");
if(referencesCustomer && Customer == null)
return;
If you gagged when seeing this code, that is a good sign. Let us solve this properly. First, we want some help from the compiler, so let us inspect the when() meta method that we have seen in the previous post a little closer.
[Meta] public static ExpressionStatement when(Expression condition, BlockExpression action) { var ctor = new BlockExpression(); var conditionFunc = new Block(); conditionFunc.Add(new ReturnStatement(condition)); ctor.Body.Add( new BinaryExpression( BinaryOperatorType.Assign, new ReferenceExpression("Condition"), new BlockExpression(conditionFunc) ) ); Expression serialize = new CodeSerializer().Serialize(condition); Builder.Revisit(serialize); ctor.Body.Add( new BinaryExpression( BinaryOperatorType.Assign, new ReferenceExpression("ConditionExpression"), serialize ) ); ctor.Body.Add( new BinaryExpression( BinaryOperatorType.Assign, new ReferenceExpression("Action"), action ) ); return new ExpressionStatement( new MethodInvocationExpression( ctor )); }
We take the cal to the when method and transform it to the following code:
delegate { Condition = () => Order.Amount > 10 && Customer.IsPreferred; ConditionExpression = (Expression<Func<bool>>)() => Order.Amount > 10 && Customer.IsPreferred; Action = delegate { // not interesting for this post }; }();
I am translating to C# 3.0 here in order to make it easier to grasp the concept. The real code is in Boo, of course, and is more interesting. The most fascinating concept here is the use of CodeSerializer, which will turn the condition that we passed into an AST that we can access at runtime. I tried to simulate that by doing an explicit cast to expression tree, which would give similar result in C#).
Having the AST of the code at runtime, even if we don't want to change it (a totally different concept) is incredibly powerful. In this case, we are going to use this to detect when we are referencing a null property and marking the rule as invalid.
Here is the code:
public void Evaluate() { var references = new List<string>(); new InlineVisitor { OnReferenceExpression = r => references.Add(r.Name); }.Visit(ConditionExpression); if(references.Contains("Customer") && Customer == null) return;// rule invalid if(Condition()) Action(); }
This is a very simple example of how you can add smarts to the way that your code behaves. This technique is the foundation for a whole host of options. I am using similar approaches for adaptive rules and for auditable actions. Fun stuff, if I say so myself.
No future posts left, oh my!