Why do F# computation expressions require a builder object (rather than a class)?

▼魔方 西西 提交于 2019-11-29 02:23:40

问题


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.


回答1:


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
}



回答2:


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.




回答3:


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!