F# computation expressions have the syntax:
ident { cexpr }
Where ident
is the builder object (this syntax is taken from Don Syme's 2007 blog entry).
In all the examples I've seen, builder objects are singleton instances, and stateless to boot. Don gives the example of defining a builder object called attempt
:
let attempt = new AttemptBuilder()
My question: Why doesn't F# just use the AttemptBuilder
class directly in computation expressions? Surely the notation could be de-sugared to static method calls just as easily as instance method calls.
Using an instance value means that one could in theory instantiate multiple builder objects of the same class, presumably parameterised in some way, or even (heaven forbid) with mutable internal state. But I can't imagine how that would ever be useful.
Update: The syntax I quoted above suggests the builder must appear as a single identifier, which is misleading and probably reflects an earlier version of the language. The most recent F# 2.0 Language Specification defines the syntax as:
expr { comp-or-range-expr }
which makes it clear that any expression (that evaluates to a builder object) can be used as the first element of the construct.
Your assumption is correct; a builder instance can be parameterized, and parameters can be subsequently used throughout the computation.
I use this pattern for building a tree of mathematical proof to a certain computation. Each conclusion is a triple of a problem name, a computation result, and a N-tree of underlying conclusions (lemmas).
Let me provide with a small example, removing a proof tree, but retaining a problem name. Let's call it annotation as it seems more suitable.
type AnnotationBuilder(name: string) =
// Just ignore an original annotation upon binding
member this.Bind<'T> (x, f) = x |> snd |> f
member this.Return(a) = name, a
let annotated name = new AnnotationBuilder(name)
// Use
let ultimateAnswer = annotated "Ultimate Question of Life, the Universe, and Everything" {
return 42
}
let result = annotated "My Favorite number" {
// a long computation goes here
// and you don't need to carry the annotation throughout the entire computation
let! x = ultimateAnswer
return x*10
}
It's just a matter of flexibility. Yes, it would be simpler if the Builder classes were required to be static, but it does take some flexibility away from developers without gaining much in the process.
For example, let's say you want to create a workflow for communicating with a server. Somewhere in the code, you'll need to specify the address of that server (a Uri, an IPAddress, etc.). In which cases will you need/want to communicate with multiple servers within a single workflow? If the answer is 'none' then it makes more sense for you to create your builder object with a constructor which allows you to pass the Uri/IPAddress of the server instead of having to pass that value around continuously through various functions. Internally, your builder object might apply the value (the server's address) to each method in the workflow, creating something like (but not exactly) a Reader monad.
With instance-based builder objects, you can also use inheritance to create type hierarchies of builders with some inherited functionality. I haven't seen anyone do this in practice yet, but again -- the flexibility is there in case people need it, which you wouldn't have with statically-typed builder objects.
One other alternative is to make use of single case discriminated unions as in :
type WorkFlow = WorkFlow with
member __.Bind (m,f) = Option.bind f m
member __.Return x = Some x
then you can directly use it like
let x = WorkFlow{ ... }
来源:https://stackoverflow.com/questions/12339223/why-do-f-computation-expressions-require-a-builder-object-rather-than-a-class