【问题标题】:Why do F# computation expressions require a builder object (rather than a class)?为什么 F# 计算表达式需要构建器对象(而不是类)?
【发布时间】:2016-06-03 21:56:59
【问题描述】:

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

ident { cexpr }

其中ident 是构建器对象(此语法取自Don Syme's 2007 blog entry)。

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

let attempt = new AttemptBuilder()

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

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


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

expr { comp-or-range-expr }

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

【问题讨论】:

标签: f# computation-expression


【解决方案1】:

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

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

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

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)中,通过将 monad 的类型定义为将问题名称作为参数的函数,可以实现相同的目的。我还没有尝试过,但在 F# 中也有可能。所以这回答了我最初的问题,但让我想知道为什么 F# 的设计者选择不强调函数链的数学上更纯粹的方法。
【解决方案2】:

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

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

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

【讨论】:

  • 感谢您提及 Reader monad。这是一个值得思考的好例子。
【解决方案3】:

另一种选择是使用区分大小写的联合,如下所示:

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

那么你就可以直接像这样使用了

let x = WorkFlow{ ... }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-02-24
    • 2015-05-30
    • 2013-09-25
    • 1970-01-01
    • 1970-01-01
    • 2018-08-14
    • 2023-03-28
    相关资源
    最近更新 更多