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

耗尽温柔 提交于 2019-11-30 04:51:28

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