Implementation of the builder template (a la System.Text.StringBuilder) in F #

The mutation state is in the center of the builder pattern. Is there an idiomatic way to implement the internal elements of such a class in F #, which reduces / eliminates the volatile state, while maintaining the usual interface (this class will be used mainly in other .NET languages)?

Here's a naive implementation:

type QueryBuilder<'T>() = //' let where = ref None let orderBy = ref None let groupBy = ref None member x.Where(cond) = match !where with | None -> where := Some(cond) | _ -> invalidOp "Multiple WHERE clauses are not permitted" // members OrderBy and GroupBy implemented similarly 

One idea is to create a record type to hold internal elements and use copy and update expressions.

 type private QueryBuilderSpec<'T> = //' { Where : ('T -> bool) option; //' OrderBy : (('T -> obj) * bool) list; //' GroupBy : ('T -> obj) list } //' type QueryBuilder<'T>() = //' let spec = ref None member x.Where(cond) = match !spec with | None -> spec := Some({ Where = Some(cond); OrderBy = []; GroupBy = [] }) | Some({ Where = None; OrderBy = _; GroupBy = _} as s) -> spec := Some({ s with Where = Some(cond) }) | _ -> invalidOp "Multiple WHERE clauses are not permitted" // members OrderBy and GroupBy implemented similarly 

All this seems a little awkward, and perhaps this should be expected when trying to implement an imperative template in F #. Is there a better way to do this, again, to keep the builder’s normal interface for the sake of imperative languages?

+4
source share
3 answers

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>) = // build query from builder here, knowing that all components have been set 

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.

+7
source

Eliminating volatility from the inside out doesn't look like it has a lot of points for me ... you make it volatile in design — any tricks at this point don't change anything.

As for brevity - let mutable is probably as good as it gets (so you don't need to use ! For dereferencing):

 type QueryBuilder<'T>() = let mutable where = None let mutable orderBy = None let mutable groupBy = None member x.Where(cond) = match where with | None -> where <- Some(cond) | _ -> invalidOp "Multiple WHERE clauses are not permitted" // members OrderBy and GroupBy implemented similarly 
+2
source

One option would be to simply use the F # record type with the default value, where it is None / empty anyway:

 type QueryBuilderSpec<'T> = { Where : ('T -> bool) option; OrderBy : (('T -> obj) * bool) list; GroupBy : ('T -> obj) list } let Default = { Where = None; OrderBy = None; GroupBy = [] } 

This allows the client code to take a new copy using the c syntax:

 let myVal = { Default with Where = fun _ -> true } 

Then you can use “c” to create additional copies of “myVal” if you want, and thus “create” additional properties, leaving the original unchanged:

 let myVal' = { myVal with GroupBy = [fun x -> x.Whatever] } 
+1
source

Source: https://habr.com/ru/post/1301351/


All Articles