I think that depending on your use cases, you might be better off with an immutable implementation. The following example will statically ensure that any builder has its own properties where, order and group, specified exactly once before building, although they can be installed in any order:
type QueryBuilder<'t,'w,'o,'g> = internal { where : 'w; order : 'o; group : 'g } with let emptyBuilder = { where = (); order = (); group = () } let addGroup (g:'t -> obj) (q:QueryBuilder<'t,_,_,unit>) : QueryBuilder<'t,_,_,_> = { where = q.where; order = q.order; group = g } let addOrder (o:'t -> obj * bool) (q:QueryBuilder<'t,_,unit,_>) : QueryBuilder<'t,_,_,_> = { where = q.where; order = o; group = q.group } let addWhere (w:'t -> bool) (q:QueryBuilder<'t,unit,_,_>) : QueryBuilder<'t,_,_,_> = { where = w; order = q.order; group = q.group } let build (q:QueryBuilder<'t,'t->bool,'t->obj,'t->obj*bool>) =
Obviously, you may have to tweak this for your specific constraints, as well as expose it to other languages that you can use for use in another class and delegate instead of constraint related functions and F # function types, but you will get an image.
UPDATE
It might be worth expanding what I did with a slightly more detailed description - the code is somewhat tight. There is nothing special about using post types; a normal immutable class will be just as good - the code will be slightly less concise, but interaction with other languages will probably work better. There are two important features in my implementation:
- Each of the add methods returns a new builder representing the current state. This is fairly simple, although it is clearly different from the way the Builder template is typically implemented.
- Using additional parameters of type generic, you can force the use of non-trivial invariants, such as the requirement to set each of several different properties exactly once before using Builder. This may be redundant for some applications, and it is a bit complicated. This is only possible with the Builder unchanged, since after the operation it may be necessary to return the Builder with various parameters.
In the above example, this sequence of operations will be resolved by the type system:
let query = emtpyBuilder |> addGroup ... |> addOrder ... |> addWhere ... |> build
whereas this will not happen, because he never sets the order:
let query = emptyBuilder |> addGroup ... |> addWhere ... |> build
As I said, this may be redundant for your application, but this is only possible because we use immutable collectors.