为什么F#计算表达式需要一个构建器对象(而不是一个类)?

17

F#计算表达式的语法如下:

ident { cexpr }

这里的ident是生成器对象(这种语法来自Don Syme 2007年的博客文章)。

在我看过的所有示例中,生成器对象都是单例实例,并且本身是无状态的。Don提供了一个定义名为attempt的生成器对象的示例:

let attempt = new AttemptBuilder()

我的问题:为什么 F# 不直接在计算表达式中使用 AttemptBuilder 类呢?毕竟,这种符号表示法可以像实例方法调用一样轻松地转换成静态方法调用。

使用实例值意味着理论上可以实例化多个相同类的构建器对象,假设以某种方式参数化,甚至(天哪)具有可变内部状态。但我想象不出这将如何有用。


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

expr { comp-or-range-expr }

这表明任何表达式(可评估为生成器对象)都可以用作结构的第一个元素。


参数化计算表达式示例:https://github.com/mausch/fsharpx/blob/ec5f2de4c1c81aea8ef5e139b10c0cd0091ac2e5/src/FSharpx.Core/Monad.fs#L201 - Mauricio Scheffer
3个回答

15
你的假设是正确的;一个构建器实例可以被参数化,这些参数可以随后在计算过程中使用。
我使用这种模式来构建到某个计算的数学证明树。每个“结论”都是一个三元组,包括“问题名称”、“计算结果”和一个“底层结论”(引理)的N-Tree。
让我提供一个小例子,删除证明树,但保留“问题名称”。我们称之为“注释”,因为它似乎更合适。
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
}

不错的例子。在Haskell中(我想OCaml也是如此),可以通过将单子的类型定义为以问题名称作为参数的函数来实现相同的效果。我没有尝试过,但在F#中也可能是可行的。所以这回答了我最初的问题,但同时让我想知道为什么F#的设计者选择减弱了更数学纯粹的函数链式方法。 - Todd Owen

6

这只是灵活性的问题。是的,如果Builder类必须是静态的,那么它会更简单,但这会剥夺开发人员的一些灵活性,而没有在过程中获得太多。

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

使用基于实例的builder对象,您还可以使用继承创建一些继承功能的builder类型层次结构。我还没有看到有人在实践中这样做,但是再次说明-灵活性是存在的,如果人们需要它,那么您就不会拥有具有静态类型的builder对象。


谢谢提到 Reader monad。这是一个很好的例子可以思考。 - Todd Owen

5

另一个选择是使用单一情况辨别联合,例如:

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

那么你可以直接像这样使用它。
let x = WorkFlow{ ... }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接