创建 'add' 计算表达式

Creating an 'add' computation expression

我想要下面的示例计算表达式和值 return 6。对于某些数字,数字并没有像我预期的那样产生。我缺少获得结果的步骤是什么?谢谢!

type AddBuilder() =
    let mutable x = 0
    member _.Yield i = x <- x + i
    member _.Zero() = 0
    member _.Return() = x

let add = AddBuilder()

(* Compiler tells me that each of the numbers in add don't do anything
   and suggests putting '|> ignore' in front of each *)
let result = add { 1; 2; 3 }

(* Currently the result is 0 *)
printfn "%i should be 6" result

注意:这只是为了创建自己的计算表达式以扩展我的学习。 Seq.sum 会是更好的方法。我认为这个例子完全忽略了计算表达式的价值,不利于学习。

这里有很多错误。

首先,让我们从简单的机制开始

为了调用Yield方法,大括号内的代码必须使用yield关键字:

let result = add { yield 1; yield 2; yield 3 }

但是现在编译器会抱怨你还需要一个Combine方法。看,yield 的语义是它们中的每一个都产生一个完成的计算,一个结果值。因此,如果您想拥有多个,则需要某种方式将它们“粘合”在一起。这就是 Combine 方法的作用。

由于您的计算构建器实际上并没有产生任何结果,而是改变了它的内部变量,因此计算的最终结果应该是该内部变量的值。这就是 Combine 需要 return:

member _.Combine(a, b) = x

但是现在编译器又报错了:你需要一个Delay方法。 Delay 不是绝对必要的,但为了减少性能缺陷,它是必需的。当计算由许多“部分”组成时(例如多个 yields 的情况),通常情况下应该丢弃其中的一些部分。在这些情况下,评估所有这些然后丢弃一些是低效的。因此,编译器插入对 Delay 的调用:它接收一个函数,该函数在被调用时将评估计算的“一部分”,并且 Delay 有机会将该函数置于某种形式延迟容器,以便稍后 Combine 可以决定丢弃哪些容器以及评估哪些容器。

然而,在您的情况下,由于计算结果并不重要(请记住:您没有 returning 任何结果,您只是在改变内部变量),Delay 可以只执行它接收到的函数以产生副作用(即 - 改变变量):

member _.Delay(f) = f ()

现在计算终于编译通过了,看吧:它的结果是6。这个结果来自 Combine 是 return 的任何东西。尝试像这样修改它:

member _.Combine(a, b) = "foo"

现在你的计算结果突然变成了"foo"


现在,让我们继续语义

以上修改将使您的程序编译通过并产生预期的结果。但是,我认为您首先误解了计算表达式的整个概念。

构建器不应该有任何内部状态。相反,它的方法应该操纵某种复杂的值,一些方法创建新值,一些修改现有值。例如,seq 构建器1 操纵 序列。这是它处理的值类型。不同的方法创建新的序列(Yield)或以某种方式对其进行变换(例如Combine),最终的结果也是一个序列。

在您的情况下,您的构建器需要操作的值似乎是数字。最后的结果也是一个数字。

那么让我们看看方法的语义。

Yield 方法应该创建您正在操作的那些值之一。由于您的值是数字,因此 Yield 应该 return:

member _.Yield x = x

如上所述,Combine 方法应该组合由表达式的不同部分创建的两个这样的值。在您的情况下,由于您希望最终结果是总和,因此 Combine 应该这样做:

member _.Combine(a, b) = a + b

最后,Delay 方法应该只执行提供的函数。在您的情况下,由于您的值是数字,因此丢弃其中任何一个都没有意义:

member _.Delay(f) = f()

就是这样!通过这三种方法,你可以添加数字:

type AddBuilder() =
    member _.Yield x = x
    member _.Combine(a, b) = a + b
    member _.Delay(f) = f ()

let add = AddBuilder()

let result = add { yield 1; yield 2; yield 3 }

我认为数字不是学习计算表达式的一个很好的例子,因为数字缺乏计算表达式应该处理的内部结构。尝试创建一个 maybe 构建器来操纵 Option<'a> 值。

额外的好处 - 您可以在线找到并使用参考的实现。


1 seq 实际上不是计算表达式。它早于计算表达式,并由编译器以特殊方式处理。但是对于示例和比较来说已经足够好了。