Haskell 的一个关键区别在于有两个同名和相同作用域的定义,以及在嵌套作用域中有两个同名定义。文件中的 GHCi vs modules 与这里的底层概念并没有真正的关系,但是如果您不熟悉这些情况,这些情况确实会导致您遇到问题。
let 表达式(和 do 块中的 let 语句)创建一个 set 具有相同范围的绑定,而不仅仅是单个绑定。例如,作为表达式:
let a = True
a = False
in a
或者用大括号和分号(不开启多行模式更方便粘贴到GHCi):
let { a = True; a = False} in a
这将失败,无论是在模块中还是在 GHCi 中。不能有一个变量a 既是True 又是False,并且在同一个范围内不能有两个名为a 的单独变量(或者不可能知道哪个变量被引用到源文本a)。
单个绑定集中的变量都是“一次”定义的;它们的写入顺序根本不相关。您可以看到这一点,因为可以定义相互引用的相互递归绑定,并且不可能以任何顺序一次定义一个:
λ let a = True : b
| b = False : a
| in take 10 a
[True,False,True,False,True,False,True,False,True,False]
it :: [Bool]
在这里,我定义了一个交替的True 和False 的无限列表,并用它来得出一个有限的结果。
Haskell 模块是一个单个范围,包含文件中的所有定义。就像在具有多个绑定的 let 表达式中一样,所有定义“同时发生”1;它们仅按特定顺序排列,因为将它们写在文件中不可避免地会引入顺序。所以在一个模块中:
a = True
a = False
如您所见,给您一个错误。
在 do-block 中有 let-statements 而不是 let-expressions。2 这些没有 in 部分,因为它们只作用于 do-block 的整个其余部分.3 GHCi 命令与在IO do-block 中输入语句非常相似,因此您在此处具有相同的选项,这就是您在示例中使用的内容。
但是您的示例有 两个 let-bindings,而不是一个。因此,在两个单独的作用域中定义了两个名为 a 的单独变量。
Haskell 不关心(几乎永远)不同定义的书面顺序,但它确实关心嵌套范围的“嵌套顺序”;规则是,当您引用变量 a 时,您将获得 a 的最内层定义,其范围包含引用。4
顺便说一句,通过在内部范围内重用名称来隐藏外部范围名称称为遮蔽(我们说内部定义遮蔽了外部定义)。这是一个有用的通用编程术语,因为这个概念出现在许多语言中。
因此,在 GHCi 与模块中,关于何时可以定义两次名称的规则并不不同,只是不同的上下文使不同的事情变得更容易。
如果你想在一个模块中放一堆定义,简单的做法是让它们都成为顶级定义,它们都具有相同的范围(整个模块),所以如果你使用你会得到一个错误两次同名。您必须多做一些工作才能嵌套定义。
在 GHCi 中,您一次输入一个命令,使用多行命令或大括号和分号样式需要更多工作,因此当您要输入多个定义时,最简单的方法是使用多个 let 语句,因此如果您重用名称,您最终会掩盖之前的定义。5您必须更加谨慎地尝试在同一范围内实际输入多个名称。
1 或者更准确地说,绑定“只是”,根本没有任何“它们发生的时间”的概念。
2 或者更确切地说:您有 let-statements 和 let-expressions,因为语句主要由表达式组成,而 let-expression 作为表达式总是有效的。
3 您可以将其视为一般规则,即 do 块中的后面语句在概念上嵌套在所有早期语句中,因为当您将它们转换为一元操作时,它们就是这个意思;实际上,let 语句实际上被转换为 let 表达式,其余的 do 块位于 in 部分中。
4 虽然不可能引用任何更远的定义。
5 请注意,您之前定义的任何引用该名称的东西在阴影之前的行为仍然完全 像以前一样,引用以前的名称。这包括返回变量值的函数。最容易将阴影理解为引入一个恰好与早期变量名称相同的不同变量,而不是试图将其理解为实际更改早期变量名称所指的内容。