开发者

Implementing the builder pattern (a la System.Text.StringBuilder) in F#

Mutating state is at the center of the builder pattern. Is there an idiomatic way to implement the internals of such a class in F# that will reduce/eliminate mutable state while retaining the usual interface (this class will be used mostly from 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 store the internals, and use copy and update expressions.

type private QueryBuilderSpec<'T> =                     //'
    { Where : ('T -> bool) option;                      //'
      OrderBy : (('T -> obj) * bool) list;              //'
      GroupBy : ('T -> obj) list }                      //'

type Quer开发者_开发技巧yBuilder<'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

This all seems a bit clunky, and maybe that should be expected when trying to implement an imperative pattern in F#. Is there a better way to do this, again, retaining the usual builder interface for the sake of imperative languages?


I think that depending on your use cases you might be better off with an immutable implementation. The following example will statically enforce that any builder has its where, order, and group properties set exactly once before being built, although they can be set 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 particular constraints, and to expose it to other languages you may want to use members on another class and delegates instead of let-bound functions and F# function types, but you get the picture.

UPDATE

Perhaps it's worth expanding on what I've done with a bit more description - the code is somewhat dense. There is nothing special about using record types; a normal immutable class would be just as good - the code would be a bit less concise but interop with other languages would probably work better. There are essentially two important features of my implementation

  1. Each of the methods for adding returns a new builder representing the current state. This is fairly straightforward, though it is obviously different from how the Builder pattern is normally implemented.
  2. By using additional generic type parameters you can enforce non-trivial invariants, such as requiring each of several different properties to be specified exactly once before using the Builder. This may be overkill for some applications, and is a bit tricky. It is only possible with an immutable Builder, since we might need to return a Builder with different type parameters after an operation.

In the example above, this sequence of operations would be allowed by the type system:

let query = 
  emtpyBuilder
  |> addGroup ...
  |> addOrder ...
  |> addWhere ...
  |> build

whereas this one wouldn't, because it never sets the order:

let query =
  emptyBuilder
  |> addGroup ...
  |> addWhere ...
  |> build

As I said, this may be overkill for your application, but it is only possible because we're using immutable builders.


Eliminating mutability "from inside" doesn't look like it has much point to me... you're making it mutable by design - any tricks at that point don't really change anything.

As for conciseness - let mutable is probably as good as it gets (so that you don't need to use ! to dereference):

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


One alterative would be to just use an F# record type, with a default value where everything is None/empty:

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 client code to take a new copy using the "with" syntax:

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

You can then make use "with" to make further copies of "myVal" if you wish, and thus "build" up further properties, while leaving the original unchanged:

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

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜