12.7.2 创建日志记录计算

来源:互联网 发布:微信数据库修复在哪里 编辑:程序博客网 时间:2024/05/17 03:50
12.7.2 创建日志记录计算



    清单 12.24 首先开始通过实现两个辅助函数,用于读取和写入控制台。两者还会把一条消息写入日志,所以,它们将括在 log 计算块中。为了显示如何组合非标准计算,我们在第三个函数中,使用了这两个函数。在我们前面的示例中,使用 let! 基元,但是,清单 12.24 还引入了 do!。



Listing 12.24 Logging using computation expressions (F# Interactive)



> let write(s) = log {
     do! logMessage("writing: " + s)
     Console.Write(s) }
val write : string -> Logging<unit>

> let read() = log {
     do! logMessage("reading")
     return Console.ReadLine() }
val read : unit -> Logging<string>

> let testIt() = log {
     do! logMessage("starting")
     do! write("Enter name: ")
     let! name = read()
     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 的幕后使用。当我们写的一个计算,不返回任何东西,F# 编译器会自动使用 Zero 的结果,作为整体结果。在讨论编译器如何代码转换为方法调用时,我们会看到,这个成员是如何使用的。

    如果你看看清单中的类型签名,可以看到,所有函数的结果类型是这个计算类型 (Logging<'T>),这与我们早前实现的 logMessage 函数的结果类型是相同的。这说明,我们有两种方法,来写非标准计算类型的函数。我们可以直接生成计算类型(像我们在 logMessage 函数中做的),或者使用计算表达式。第一种情况主要用于写基元;第二情况用于从这些基元或其他函数组合代码。

    从 testIt 函数中,你可以看到计算表达式的可组合性质。首先,它使用 do! 结构,去调用直接实现的基元函数。写到屏幕(和日志) 使用计算表达式实现,但是,以完全相同的方式调用它。当我们调用一个函数,它返回值,并将写入到日志中,所以,我们使用有 let! 关键字的自定义的绑定。

    事实上,没有必要了解编译器如何将计算表达转换成方法调用,但是,如果你是好奇,清单 12.25 显示了从前面的清单中转换后的代码,包括 Zero 成员的使用和 do! 基元的转换。



Listing 12.25 Translated version of the logging example (F#)



let write(s) =
  log.Bind(logMessage("writing: " + s), fun () –>
    Console.Write(s)
    log.Zero())

let read() =
  log.Bind(logMessage("reading"), fun () –>
    log.Return(Console.ReadLine()))

let testIt() =
  log.Bind(logMessage("starting"), fun () –>
    log.Bind(write("Enter name: "), fun () –>
    log.Bind(read(), fun name –>
      log.Return("Hello " + name + "!"))))



    Zero 基元只用在 write 函数中,因为这是唯一的地方,我们从函数中不返回任何结果。在其他两个函数中,最里面的调用是对 Return 成员的,它取一个简单的值作为参数值,并把它打包到 LoggingValue <'T> 类型中,它不包含任何日志消息。

    如你所见,当转换计算表达式时,每个 do! 或 let! 的使用,用对 Bind 成员的调用替换。如果你还记得我们早些时候讨论的序列表达式,就可以看到现在的相似之处。在序列表达式中,每一个 for 循环被转换成对 Seq.collect 的调用。我们可以更进一步地类比,因为 Return 基元对应于创建只包含一个元素的序列,Zero 基元对应于可能返回空序列的序列表达式。

    还有一个有趣点,需要我们突出关注。再看看清单 12.24 的原始代码,可以看到,它看起来就像普通 F# 代码,只是加了几个 !符号,容易把普通的 F# 代码打包成计算表达式。