Lean 函数式编程

2.5. 额外的便利功能🔗

2.5.1. 嵌套活动🔗

feline 中的许多函数都表现出一种重复模式,其中 IO 活动的结果被赋予一个名称,然后立即且仅使用一次。 例如,在 dump 中:

partial def dump (stream : IO.FS.Stream) : IO Unit := do let buf stream.read bufsize if buf.isEmpty then pure () else let stdout IO.getStdout stdout.write buf dump stream

模式出现在 stdout 中:

let stdout IO.getStdout stdout.write buf

类似地,fileStream 包含以下代码片段:

let fileExists filename.pathExists if not fileExists then

当 Lean 编译 do 块时,由括号下紧跟左箭头组成的表达式被提升到最近的封闭 do,它们的结果被绑定到唯一的名称。 这个唯一的名称替换表达式的原始位置。 这意味着 dump 也可以写成如下形式:

partial def dump (stream : IO.FS.Stream) : IO Unit := do let buf stream.read bufsize if buf.isEmpty then pure () else ( IO.getStdout).write buf dump stream

这个版本的 dump 避免了引入只使用一次的名称,这可以大大简化程序。 Lean 从嵌套表达式上下文中提升的 IO 活动称为嵌套活动

fileStream 可以使用相同的技术来简化:

def fileStream (filename : System.FilePath) : IO (Option IO.FS.Stream) := do if not ( filename.pathExists) then ( IO.getStderr).putStrLn s!"File not found: {filename}" pure none else let handle IO.FS.Handle.mk filename IO.FS.Mode.read pure (some (IO.FS.Stream.ofHandle handle))

在这种情况下,handle 的局部名称也可以使用嵌套活动来消除,但生成的表达式会很长且复杂。 尽管使用嵌套活动通常是好的风格,但有时命名中间结果仍然是有帮助的。

然而,重要的是要记住,嵌套活动只是在周围的 do 块中出现的 IO 活动的简短记法。 执行它们所涉及的副作用仍然以相同的顺序发生,副作用的执行不与表达式的求值交织在一起。 因此,嵌套活动不能从 if 的分支中提升。

作为这可能令人困惑的示例,考虑以下辅助定义,它们在向世界宣布已被执行后返回数据:

def getNumA : IO Nat := do ( IO.getStdout).putStrLn "A" pure 5def getNumB : IO Nat := do ( IO.getStdout).putStrLn "B" pure 7

这些定义旨在替代更复杂的 IO 代码,这些代码可能验证用户输入、读取数据库或打开文件。

当数字 A 为五时打印 0,否则打印数字 B 的程序可能如下所示:

def test : IO Unit := do let a : Nat := if ( getNumA) == 5 then 0 else (invalid use of `(<- ...)`, must be nested inside a 'do' expression getNumB) ( IO.getStdout).putStrLn s!"The answer is {a}"

这个程序等效于:

def test : IO Unit := do let x getNumA let y getNumB let a : Nat := if x == 5 then 0 else y ( IO.getStdout).putStrLn s!"The answer is {a}"

它无论 getNumA 的结果是否等于 5 都会运行 getNumB。 为了防止这种混淆,嵌套活动不允许在不是 do 中的行的 if 中使用,并产生以下错误消息:

invalid use of `(<- ...)`, must be nested inside a 'do' expression

2.5.2. do 的灵活布局🔗

在 Lean 中,do 表达式对空白敏感。 do 中的每个 IO 活动或局部绑定都应该在自己的行上开始,并且它们都应该有相同的缩进。 几乎所有的 do 使用都应该这样写。 然而,在一些罕见的情况下,可能需要手动控制空白和缩进,或者在单行上有多个小活动可能会很方便。 在这些情况下,换行符可以用分号替换,缩进可以用大括号替换。

例如,以下所有程序都是等效的:

-- This version uses only whitespace-sensitive layout def main : IO Unit := do let stdin IO.getStdin let stdout IO.getStdout stdout.putStrLn "How would you like to be addressed?" let name := ( stdin.getLine).trim stdout.putStrLn s!"Hello, {name}!"-- This version is as explicit as possible def main : IO Unit := do { let stdin IO.getStdin; let stdout IO.getStdout; stdout.putStrLn "How would you like to be addressed?"; let name := ( stdin.getLine).trim; stdout.putStrLn s!"Hello, {name}!" }-- This version uses a semicolon to put two actions on the same line def main : IO Unit := do let stdin IO.getStdin; let stdout IO.getStdout stdout.putStrLn "How would you like to be addressed?" let name := ( stdin.getLine).trim stdout.putStrLn s!"Hello, {name}!"

地道的 Lean 代码很少在 do 中使用大括号。

2.5.3. 使用 #eval 运行 IO 活动🔗

Lean 的 #eval 命令可以用来执行 IO 活动,而不仅仅是求值它们。 通常,向 Lean 文件添加 #eval 命令会使 Lean 求值提供的表达式,将结果值转换为字符串,并在工具提示和信息窗口中提供该字符串。 与因为 IO 活动无法转换为字符串而失败不同,#eval 执行它们,执行它们的副作用。 如果执行的结果是 Unit(),则不显示结果字符串,但如果它是可以转换为字符串的类型,则 Lean 显示结果值。

这意味着,给定 countdownrunActions 的先前定义,

3 2 1 Blast off! #eval runActions (countdown 3)

会显示

3
2
1
Blast off!

这是运行 IO 活动产生的输出,而不是活动本身的某种不透明表示。 换句话说,对于 IO 活动,#eval求值提供的表达式又执行结果活动值。

使用 #eval 快速测试 IO 活动比编译和运行整个程序要方便得多。 然而,存在一些限制。 例如,从标准输入读取只是返回空输入。 此外,每当 Lean 需要更新它提供给用户的诊断信息时,IO 活动就会重新执行,这可能在不可预测的时间发生。 例如,读取和写入文件的活动可能会意外地这样做。