Haskell 源文件和 GHCi 中变量定义的区别?

difference between variable definition in a Haskell source file and in GHCi?

在 Haskell 源文件中,我可以写

a = 1

我的印象是我必须在 GHCi 中编写与

相同的内容
let a = 1

,对于 GHCi 中的 a = 1,在 = 上给出了一个解析错误。

现在,如果我写

a = 1
a = 2

在源文件中,我会得到关于 a 的多重声明的错误,但是在 GHCi 中写 OK:

let a = 1
let a = 2

谁能帮忙弄清楚这两种风格的区别?

交互式解释器中连续的let "statements" 实际上相当于嵌套的let 表达式。它们的行为就好像在赋值之后有一个隐含的 in,而解释器会话的其余部分包含 let 的主体。即

>>> let a = 1
>>> let a = 1
>>> print a

相同
let a = 1 in
let a = 1 in
print a

Haskell 中的一个关键区别在于具有相同名称和相同范围的两个定义,以及在嵌套范围中具有相同名称的两个定义。 GHCi vs 文件中的模块与这里的基本概念并没有真正的关系,但是如果你不熟悉这些情况确实会导致你遇到问题。

一个 let 表达式(和 do 块中的一个 let 语句)创建一个 set 具有相同作用域的绑定,而不仅仅是一个绑定。例如,作为表达式:

let a = True
    a = False
in  a

或者用大括号和分号(不开启多行模式粘贴到GHCi中更方便):

let { a = True; a = False} in a

这将失败,无论是在模块中还是在 GHCi 中。不能有一个变量 a 同时是 TrueFalse,并且在同一范围内不能有两个名为 a 的单独变量(或者它会是不可能知道源文本指的是哪一个 a).

单个绑定集中的变量都已定义"at once";他们写的顺序根本不相关。您可以看到这一点,因为可以定义相互引用的相互递归绑定,并且不可能以任何顺序一次定义一个:

λ let a = True : b
|     b = False : a
| in  take 10 a
[True,False,True,False,True,False,True,False,True,False]
it :: [Bool]

这里我定义了一个无限列表 TrueFalse,并用它得出一个有限的结果。

一个Haskell模块是一个单一作用域,包含文件中的所有定义。就像在具有多个绑定的 let 表达式中一样,所有定义 "happen at once"1;它们只是按特定的顺序排列,因为将它们写在文件中不可避免地会引入一个顺序。所以在一个模块中:

a = True
a = False

如您所见,给您一个错误。

在 do 块中,您有 let 语句而不是 let 表达式。2 这些没有 in 部分,因为它们只是范围do-block 的其余部分。3 GHCi 命令非常类似于在 IO do-block 中输入语句,所以你在那里有相同的选项,这就是你的在您的示例中重新使用。

但是你的例子有两个 let-bindings,没有一个。因此,在两个独立的范围内定义了两个名为 a 的独立变量。

Haskell 不关心(几乎永远)不同定义的书写顺序,但 关心嵌套范围的 "nesting order" ;规则是,当您引用变量 a 时,您会得到 a 的最内层定义,其范围包含引用。4

顺便说一句,通过在内部范围内重用名称来隐藏外部范围名称被称为 shadowing(我们说内部定义隐藏外部定义)。这是一个有用的通用编程术语,因为这个概念出现在许多语言中。

所以并不是 GHCi 和模块中关于何时可以定义两次名称的规则不同,只是不同的上下文使不同的事情变得更容易。

如果你想在一个模块中放置一堆定义,最简单的方法就是让它们都成为顶级定义,它们都具有相同的范围(整个模块),所以你会得到一个错误如果你两次使用同一个名字。您必须多做一些工作来嵌套定义。

在 GHCi 中,您一次输入一个命令,使用多行命令或大括号和分号样式会更麻烦,因此当您想输入多个定义时,最简单的事情是使用多个 let 语句,因此如果您重复使用名称,您最终会隐藏较早的定义。5 您必须更加谨慎地尝试在同一范围内实际输入多个名称。


1 或者更准确地说,绑定 "just are" 根本没有任何 "the time at which they happen" 的概念。

2 或者更确切地说:你有 let 语句和 let 表达式,因为语句主要由表达式组成,而 let 表达式始终作为表达式有效.

3 您可以将此视为一般规则,即 do 块中后面的语句在概念上嵌套在所有较早的语句中,因为当您将它们翻译成单子操作;事实上,let 语句实际上被翻译成 let 表达式,其余的 do 块在 in 部分。

4 它不像在同一范围内具有相同名称的两个变量那样模棱两可,尽管不可能引用任何进一步的定义。

5 并且请注意,您之前定义的任何在阴影之前引用名称的内容仍将像以前一样完全 ,指的是以前的名字。这包括 return 变量值的函数。将阴影理解为引入一个 different 变量恰好与早期变量同名,而不是试图将其理解为实际更改早期变量名称所指的内容。