具有自定义运算符的 F# 嵌套计算表达式

Mat*_*ews 3 f# computation-expression

我正在创建一个用于建模的 DSL,并且我希望能够创建一个Settings具有两个自定义操作的构建器:BufferConstraint,它们本身就是计算表达式。原因是该域中的术语严重过载,而计算表达式允许您通过使用自定义操作来提供上下文。

我不知道如何让这种嵌套按预期工作。我在代码示例的底部提供了一个示例,说明了我想要的结果。

type Buffer =
    {
        Name : string
        Capacity : float
    }

type Constraint =
    {
        Name : string
        Limit : float
    }

[<RequireQualifiedAccess>]
type Setting =
    | Buffer of Buffer
    | Constraint of Constraint


type BufferBuilder (name: string) =
    member _.Yield _ : Buffer = { Name = name; Capacity = 0.0 }
    member _.Run x : Buffer = x

    [<CustomOperation("Capacity")>]
    member _.Capacity (b: Buffer, newCapacity) =
        { b with Capacity = newCapacity }

let Buffer = BufferBuilder

type ConstraintBuilder (name: string) =
    member _.Yield _ : Constraint = { Name = name; Limit = 0.0 }
    member _.Run x : Constraint = x

    [<CustomOperation("Limit")>]
    member _.Limit (b: Constraint, newLimit) =
        { b with Limit = newLimit }

let Constraint = ConstraintBuilder

type SettingsBuilder () =

    member _.Yield _ : Setting list = []
    member _.Run x : Setting list = x

    [<CustomOperation("Buffer")>]
    member _.Buffer (settings, name: string, expr) =
        // This does not work
        let newSetting = BufferBuilder name expr
        newSetting :: settings

    [<CustomOperation("Constraint")>]
    member _.Constraint (settings, name: string, expr) =
        // This does not work
        let newSetting = ConstraintBuilder name expr
        newSetting :: settings


// The Computation Expression does not work
let mySettings =
    SettingsBuilder {
        Buffer "b1" {
            Capacity 100.0
        }
        Constraint "c1" {
            Limit 10.0
        }
    }

// Below shows that the desired outcome of `mySettings` would be
let b1 = { Name = "b1"; Capacity = 100.0 }
let c1 = { Name = "c1"; Limit = 10.0 }

let desiredSettings = [
    Setting.Buffer b1
    Setting.Constraint c1
]
Run Code Online (Sandbox Code Playgroud)

Fyo*_*kin 9

CE 不是这样工作的。当您编写时foo { ... }foo该表达式中的位不是函数。特别是,这意味着您不能执行以下操作:

let x = { ... }
let y = foo x
Run Code Online (Sandbox Code Playgroud)

或这个:

let f x = foo x
let y = f { ... }
Run Code Online (Sandbox Code Playgroud)

不是那样的。这是特殊的语法,而不是函数调用。大括号前面的东西必须是一个 CE对象,并且在其上定义了所有 CE 方法。

因此,特别是,这意味着您的SettingsBuilder.Buffer函数无法接受expr然后将其传递给BufferBuilder. BufferBuilder必须紧接在花括号前面。

这意味着该SettingsBuilder.Buffer函数应该接受任何结果BufferBuilder然后,在 CE 内部,您应该使用构建该结果BufferBuilder,并且仅在将其传递给Buffer自定义操作之后:

    [<CustomOperation("Buffer")>]
    member _.Buffer (settings, b) =
        (Setting.Buffer b) :: settings

    [<CustomOperation("Constraint")>]
    member _.Constraint (settings, c) =
        (Setting.Constraint c) :: settings

    member _.Zero () = []

...

let mySettings =
    SettingsBuilder () {
        Buffer (BufferBuilder "b1" {
            Capacity 100.0
        })
        Constraint (ConstraintBuilder "c1" {
            Limit 10.0
        })
    }
Run Code Online (Sandbox Code Playgroud)

(请注意,您还必须定义Zero以提供表达式的“初始”值)

()(还要注意后面的单位SettingsBuilder。没有它,SettingsBuilder是一个类,但你需要大括号左边的东西是一个对象)

我知道您想要像这样的漂亮语法,Buffer "foo" { ... }而不是其中额外BufferBuilder且丑陋的括号,但我认为这是不可能的。一般来说,没有办法让自定义操作表现得像“嵌套”表达式。


考虑另一种方法:放弃外部 CE,并将设置定义为列表,每个元素都使用其相应的 CE 构建。

您将需要这些内部 CE 来分别生成 a ,Setting以便它们的结果可以是同一列表的元素。Run这可以通过修改它们的方法以将结果值包装在相关构造函数中来实现Setting

type BufferBuilder (name: string) =
    ...
    member _.Run x = Setting.Buffer x

type ConstraintBuilder (name: string) =
    ...
    member _.Run x = Setting.Constraint x

...

let mySettings = [
    BufferBuilder "b1" {
        Capacity 100.0
    }
    ConstraintBuilder "c1" {
        Limit 10.0
    }
]
Run Code Online (Sandbox Code Playgroud)