OCaml 中的不可变变量

Immutable variables in OCaml

我正在学习 OCaml,我对变量的不变性有点困惑。根据我正在阅读的书,变量是不可变的。到目前为止一切顺利,但我到底为什么可以这样做:

let foo = 42
let foo = 4242

我错过了什么??

名称 foo 首先绑定到一个不可变值 42,然后重新绑定到另一个不可变值 4242。您甚至可以将相同的名称绑定到不同类型的变量。在 OCaml 中,我们谈论的不是变量的可变性,而是值的可变性。例如,如果将 foo 绑定到值数组,这将是相同的名称,但绑定到可变数据,以便变量的值可以及时更改。最后,每个新的绑定只是隐藏了前一个,所以原来的 foo 仍然绑定到 42,但它是不可见的,并且会被垃圾收集。

也许一个小例子可以阐明这个想法:

let example () = 
  let foo = 42     in (* 1 *)
  let foo = 4242   in (* 2 *)
  let foo = [|42|] in (* 3 *)
  foo.(0) <- 56       (* 4 *)

拥有以下心智模型可能更容易:

                  (*1*)  +--------+
                  +----> |   42   |
+------------+    |      +--------+
|            +----+
|    foo     +----+      +--------+
|            |    +----> |  4242  |
+---------+--+    (*2*)  +--------+
          |
          |       (*3*)  +--------+
          +------------> |[| 42 |]|
                  (*4*)  +--------+

在行 12 中,我们只是将变量 foo 绑定到两个不同的值。在 3 行,我们将它绑定到一个包含一个元素的数组。在行 4 上,我们更改了值,foo 仍然绑定到相同的值,但该值包含不同的数据。

我希望我没有让你更加困惑 ;)

我认为最好的解释方式是举个例子。考虑这段代码(在 OCaml REPL 中执行):

# let foo = 42;;
val foo : int = 42

# let return_foo () = foo;;
val return_foo : unit -> int = <fun>

# let foo = 24;;
val foo : int = 24

# return_foo ();;
- : int = 42

以上代码执行以下操作:

  1. 42 绑定到名称 foo
  2. 创建函数 return_foo (),returns 值绑定到 foo
  3. 24 绑定到名称 foo(这隐藏了之前 foo 的绑定)。
  4. 调用 return_foo () 函数,其中 returns 42.

将此与可变值的行为(在 OCaml 中使用 ref 创建)进行比较:

# let foo = ref 42;;
val foo : int ref = {contents = 42}

# let return_foo () = !foo;;
val return_foo : unit -> int = <fun>

# foo := 24;;
- : unit = ()

# return_foo ();;
- : int = 24

其中:

  1. 创建一个包含 42 的可变引用并将其绑定到名称 foo.
  2. 创建一个函数 return_foo (),该函数 returns 存储在绑定到 foo 的引用中的值。
  3. 24 存储在绑定到 foo 的引用中。
  4. 调用 return_foo () 函数,returns 24.

let的通常形式是let ... in表达式,在这里你定义了一个变量绑定,它只存在于let的主体内部。 let 的正文是一个新范围。

let x = 42 in (* body here *)

这里的let的"body"是一个新的作用域,和外面的不同,所有的外部变量都多了一个局部变量x也就是仅在此 let.

的正文中定义

现在您谈论的是文件顶层的 lets。这些在语法上看起来有点不同(没有 in),但实际上它们是相同的,"body" 是文件的其余部分。所以在这里你可以把 let 之后的文件的其余部分看作一个新的作用域,x 是这个作用域的局部变量。所以你的代码等同于:

let foo = 42 in (
  let foo = 4242 in (
    (* rest of file *)
  )
)

在这里,您的内部 let 绑定了一个与外部范围中已存在的变量同名的局部变量。那没关系。您正在内部范围内绑定一个新变量。如果它碰巧与外部作用域中的变量同名,那么内部作用域中引用该名称的代码将引用最内层的绑定。然而,这两个变量是完全独立的。

在类似 C 的语言中,它会是这样的:

{
    const int foo = 42;
    {
        const int foo = 4242;
        // rest of code here
    }
}

看到了吗?这里没有给任何变量赋值。