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 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
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?
The StringBuilder class has the word "builder" in its name, but it has nothing to do with telescoping constructors, it simply helps us collect all the data that we need to pass to the constructor of an immutable object.
Different structure. String Builder has two collaborators, Builder pattern has four collaborators.
Builder pattern aims to “Separate the construction of a complex object from its representation so that the same construction process can create different representations.” It is used to construct a complex object step by step and the final step will return the object.
What is a builder function? 🤔 A builder is a Flutter design pattern in which the construction code of a widget is defined outside of its class. Builder functions are callback interfaces that pass data (often layout-specific) to the parent widget which returns a child based on that data.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With