清单 12.24 首先实现两个辅助函数,用于读写控制台,且两者还会把消息写入日志,所以,它们将括在 log 计算块中。为了显示如何组合非标准计算,我们在第三个函数中,使用了两个函数。在前面的示例中,我们使用 let! 基本操作,在清单 12.24 还引入了 do!。
清单 12.24 使用计算表达式的日志 (F# Interactive)
> let write(s) = log { [1] <-- 写字符串到控制台和日志
do!logMessage("writing: " + s)
Console.Write(s) }
val write : string ->Logging<unit>
> let read() = log {
do!logMessage("reading")
returnConsole.ReadLine() }
val read : unit -> Logging<string>
> let testIt() = log {
do! logMessage("starting") [2] <-- 调用日志函数基本操作
do!write("Enter name: ") [3]<-- 调用另一个计算表达式
let! name = read() [4] <-- 使用自定义的值绑定
return "Hello" + name + "!" }
val testIt : unit ->Logging<string>
> let res = testIt();;
Enter name: Tomas
> let (Log(msg, logs)) = res;;
val msg : string = "Hello Tomas!"
val logs : string list =["starting"; "writing: Enter name:"; "reading"]
如果运行清单中的代码,它会等待控制台的输入,这在 Visual Studio 中的 F# Interactive 环境下,不能运行,所以,需要在独立命令行控制台中运行代码。我们在几个地方用到了新的 do! 基本操作,调用返回 Logging<unit> 的函数。在这里,我们使用了非标准绑定,执行 Bind 成员,因为我们想把日志记录消息连接起来。我们可以忽略实际值,因为它是 unit,这正是 do! 基本操作的行为。事实上,当我们使用 do! f () 时,它是 let! () = f() 的缩写,即,使用自定义绑定,忽略返回
unit 的值。
实现计算生成器时,我们添加了一个成员 Zero,它在清单 12.24 的幕后使用。不返回任何值的计算[1],F# 编译器会自动使用 Zero 的结果,作为整体结果。在讨论编译器把代码转换为方法调用时,我们会看到,这个成员是如何使用的。
如果我们看一下清单中的类型签名,就会发现,所有函数的结果类型都是计算类型(Logging<‘T>),与我们前面实现的 logMessage 函数的结果类型相同。这说明,我们有两种方法写非标准计算类型函数。可以直接生成计算类型(像我们在 logMessage 函数中所做的),或者使用计算表达式。第一种情况主要用于写基本操作;第二种方法用于写组合基本操作或函数的代码。
从 testIt 函数中可以发现,计算表达式具有可组合的特性。首先,使用 do! 结构,直接调用实现的基本操作函数[2];写到屏幕(和日志),是使用计算表达式实现的,而我们调用它的方式完全相同[3];当我们调用返回值并将写到日志中的函数,是使用有 let! 关键字的自定义的绑定[4]。
事实上,没有必要了解编译器如何将计算表达转换成方法调用,但是,如果你有兴起,清单 12.25 显示了前面清单转换后的代码,包括 Zero 成员的使用和 do! 基本操作的转换。
清单 12.25 日志示例的转换版本 (F#)
let write(s) =
log.Bind(logMessage("writing:" + s), fun () –>
Console.Write(s)
log.Zero()) [1] <-- 自动使用 zero 作为结果
let read() =
log.Bind(logMessage("reading"), fun () –>
log.Return(Console.ReadLine()))
let testIt() =
log.Bind(logMessage("starting"), fun () –> | [2]
log.Bind(write("Entername: "), fun () –> | 把多个绑定转换成
log.Bind(read(), funname –> | 嵌套调用
log.Return("Hello " + name + "!"))))
Zero 基本操作只在 write 函数中使用[1],因为这是唯一从函数中不返回任何结果的地方。在其他两个函数中,最里面的调用是 Return 成员,参数为一个简单的值,把它打包到 LoggingValue <‘T> 类型中,不包含任何日志消息。
可以发现,转换计算表达式时,每个 do! 或 let! 的使用,都用调用 Bind 成员替换[2]。如果我们回忆早前有关序列表达式的讨论,就会看到现在的相似之处,在序列表达式中,每一个 for 循环转换成对 Seq.collect 的调用。我们可以更进一步地类比,因为 Return 基本操作对应于创建只包含一个元素的序列,对序列表达式的Zero 基本操作可能返回空序列。
还有一点需要我们突出关注。如果再看看清单 12.24 的原始代码,就会发现,它看起来就像普通 F# 代码,只是加了几个感叹号(!),把普通的 F# 代码打包成计算表达式,非常容易。