17

F# 计算表达式具有以下语法:

ident { cexpr }

builder 对象在哪里ident(此语法取自Don Syme 的 2007 年博客条目)。

在我见过的所有示例中,构建器对象都是单例实例,并且是无状态启动的。Don 给出了定义一个名为 builder 对象的示例attempt

let attempt = new AttemptBuilder()

我的问题:为什么 F# 不AttemptBuilder直接在计算表达式中使用该类?当然,这种表示法可以像实例方法调用一样容易地用于静态方法调用。

使用实例值意味着理论上可以实例化同一类的多个构建器对象,可能以某种方式参数化,甚至(天堂禁止)具有可变的内部状态。但我无法想象这将如何有用。


更新:我上面引用的语法表明构建器必须显示为单个标识符,这具有误导性,并且可能反映了该语言的早期版本。最新的F# 2.0 语言规范将语法定义为:

expr { comp-or-range-expr }

这清楚地表明,任何表达式(计算结果为构建器对象)都可以用作构造的第一个元素。

4

3 回答 3

15

你的假设是正确的;构建器实例可以被参数化,并且参数可以随后在整个计算中使用。

我使用这种模式为某个计算构建数学证明树。每个结论都是问题名称计算结果基础结论(引理)的 N 树的三元组。

让我提供一个小例子,删除一个证明树,但保留一个问题名称。我们称它为注解,因为它看起来更合适。

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
}
于 2012-09-09T15:14:17.803 回答
6

这只是灵活性的问题。是的,如果 Builder 类被要求是静态的,这会更简单,但它确实会从开发人员那里获得一些灵活性,而不会在此过程中获得太多收益。

例如,假设您要创建一个与服务器通信的工作流。在代码的某处,您需要指定该服务器的地址(Uri、IPAddress 等)。在哪些情况下,您需要/想要在单个工作流程中与多个服务器进行通信?如果答案是“无”,那么使用构造函数创建构建器对象更有意义,该构造函数允许您传递服务器的 Uri/IPAddress,而不必通过各种函数连续传递该值。在内部,您的构建器对象可能会将值(服务器的地址)应用于工作流中的每个方法,从而创建类似于(但不完全是)Reader monad 的东西。

使用基于实例的构建器对象,您还可以使用继承来创建具有某些继承功能的构建器的类型层次结构。我还没有看到任何人在实践中这样做,但同样 - 灵活性是存在的,以防人们需要它,这是静态类型的构建器对象所没有的。

于 2012-09-09T14:27:19.537 回答
5

另一种选择是使用单例区分联合,如:

type WorkFlow = WorkFlow with
    member __.Bind (m,f) = Option.bind f m
    member __.Return x = Some x

然后你可以直接使用它

let x = WorkFlow{ ... }
于 2016-06-03T13:56:59.010 回答