这个计算将产生一个值,并能够将消息写入到本地日志记录缓冲区。这样,计算的结果将成为一个值,和包含消息的字符串列表。同样,我们使用只有一个识别器的差别联合,表示这个类型:
type Logging<‘T> =
| Log of ‘T * list<string>
这个类型非常类似于我们先前讨论的 ValueWrapper <‘a> 示例,只是加上了一个 F# 列表,表示写到日志的消息。现在,我们已有了类型,就可以实现计算生成器了。通常,我们需要实现 Bind 和 Return 成员;此外,还会实现一个新成员Zero,能够写不返回任何值的计算。后面,我们将会看到它是如何使用的。
清单 12.23 是这个生成器的实现。最重要的是Bind 成员,它把原始值的日志消息,和由计算的其余部分(是给 Bind 成员参数的函数)生成的值,连接起来。
清单 12.23 增加了日志功能的计算生成器 (F#)
type LoggingBuilder() =
member x.Bind(Log(value, logs1), f)= [1]
let (Log(newValue,logs2)) = f(value) [2]
Log(newValue, logs1 @logs2) [3]
member x.Return(value) =
Log(value, []) <-- 增加了空日志值
member x.Zero() =
Log((), []) <-- 不返回空日志值
let log = new LoggingBuilder()
与其他例子一样,最困难的部分是实现 Bind 成员。我们的日志记录类型遵循所有正常的步骤,包括option 和 ValueWrapper 类型,和省略的第三步:
1、我们要提取值。因为使用只有一种情况的差别联合,在成员的参数列表中[1],可以使用模式匹配;
2、如果有值需要计算,就调用计算的其余部分。在清单 12.23 中,总是有值,因此,可以运行给定的函数[2]。我们没有立即返回结果,而是先分解,得到在执行过程中产生的新的值和日志消息。
3、我们已经收集了两个日志消息的缓冲,所以,需要打包这个新的值,并为它添加新的日志状态。若要创建新状态,我们把原始的消息列表,和在调用计算的其余部分时生成的新列表连接起来[3]。使用列表连接运算符(@)。
Return 和 Zero 成员很简单。Return 需要把实际值打包到Logging<‘T> 类型中,Zero 表示不携带任何值的计算(即,返回 unit)。在这两种情况下,我们都将创建新的计算值,因此,基本操作返回空的日志记录缓冲。所有的日志消息将以其它方式产生,并在Bind 成员中追加。如果你看到了到目前为止我们写的代码,没有任何办法,可以创建非空的日志!因此,我们需要创建一个额外的基本操作,以创建包含日志消息的计算值。可以写成一个简单的函数:
> let logMessage(s) =
Log((), [s])
val logMessage : string ->Logging<unit>
这个函数创建一个包含 unit 值的计算值,更重要的是,还包含在日志记录的缓冲中的消息,因此,如果我们使用 Bind,把它与另外的计算组合起来,就能得到写到日志中的计算。现在,我们终于可以写代码,使用新创建的日志记录计算。