可变值和不可变值的重新定义有什么区别?

4
我已经了解到F#中的值是不可变的。然而,我也遇到过重新定义值定义的概念,这会遮蔽以前的定义。这与可变值有什么不同呢?我问这个问题不仅仅是作为一个理论构造,还想知道何时使用可变值,何时重新定义表达式;或者是否有人能指出后者不是F#的惯用语。
重新定义的基本示例:
let a = 1;;
a;; //1
let a = 2;;
a;; //2

更新1:

除下面的答案之外,F#交互环境中顶层的重定义只允许在不同的终止符中进行。以下代码也会在fsi中报错:

let a = 1
let a = 2;;

Error: Duplicate definition of value 'a'

另一方面,在let绑定中允许重新定义。更新2:实际上的区别在于,闭包无法使用可变变量。
let f =
   let mutable a = 1
   let g () = a //error
   0  
f;;

更新3:

尽管我可以使用refs对副作用进行建模,例如:

let f =
   let  a = ref 1
   let g = a
   a:=2
   let x = !g  + !a
   printfn "x: %i" x //4

f;;

除了与闭包的使用方式不同之外,我无法完全看出重新定义和使用可变关键字之间的实际区别:

let f  =
   let a = 1
   let g  = a
   let a = 2
   let x = g + a
   printfn "x: %i" x //3

f;;

vs

let f =
   let mutable a = 1
   let g = a
   a <-2
   let x = g  + a
   printfn "x: %i" x //3
 f;;

另一种思路:我不确定如何处理线程,但(a)另一个线程可以在let绑定内改变可变变量的值吗?(b)另一个线程可以重新绑定/重新定义let绑定中的值名称吗?我肯定在这里漏掉了什么。

更新4: 最后一种情况的区别在于,突变仍将从嵌套作用域进行,而在嵌套作用域中的重新定义/重新绑定将“遮盖”来自外部作用域的定义。

let f =
   let mutable a = 1
   let g = a
   if true then
      a <-2   
   let x = g  + a
   printfn "x: %i" x //3

f;;

vs

let f =
   let a = 1
   let g = a
   if true then
      let a = 2  
      printfn "a: %i" a   
   let x = g  + a
   printfn "x: %i" x //2
f;;
4个回答

5

我不熟悉特定的F#语言,但我可以回答“理论”部分。

改变对象的值可能会导致一个全局可见的副作用。任何具有对同一对象引用的其他代码都将观察到该更改。任何在程序中建立依赖于该对象价值的属性现在都可能被更改。例如,如果您以影响其排序位置的方式改变了列表中引用的对象,则已在程序的任何位置建立的任何属性都可能被更改,即使该列表已排序也可能会变为错误。这可能是一个极其不明显和非本地化的效果——处理排序列表的代码和执行突变的代码可能位于完全不同的库中(都没有直接依赖于其他),仅通过一长串调用连接(其中一些可能是由其他代码设置的闭包)。如果您广泛使用变异,则两个位置之间可能没有直接的调用链链接,而是取决于程序迄今为止执行的特定操作序列,这个可变对象最终被传递给那个变异代码的事实。

另一方面,在不可变值之间重新绑定本地变量可能从技术上仍然被视为“副作用”(取决于语言的确切语义),但它是相当局限的。因为它只对名称而不是之前或之后的值产生影响,所以不管对象来自哪里或在此之后将去哪里都无关紧要。它仅改变访问名称的其他代码的含义;您必须仔细检查受此影响的代码的位置仅限于名称的范围内。这是一种非常容易保持在方法/函数/任何内容的内部的副作用,因此即使从外部的角度来看,该函数仍然是没有副作用(纯粹的;参考透明)-事实上,如果没有捕获名称而不是值的闭包,我认为这种局部重新绑定是不可能成为外部可见的副作用。


很棒的理论概述.. 关于闭包捕获名称的一些内容,我想知道它对F#中的可变值有什么影响。让我来测试一下。 - user3056677

4

我不确定我是否同意某些答案。

以下内容在FSI和实际汇编中都可以完美地编译和执行:

let TestShadowing() =
   let a = 1
   let a = 2
   a

但是重要的是要理解正在发生的不是变异,而是遮蔽。换句话说,'a'的值没有被重新分配。另一个'a'已声明并拥有其自己的不可变值。为什么区别很重要?考虑当在内部块中遮蔽'a'时会发生什么:

let TestShadowing2() =
   let a = 1
   printfn "a: %i" a
   if true then
      let a = 2
      printfn "a: %i" a
   printfn "a: %i" a

> TestShadowing2();;
a: 1
a: 2
a: 1

在这种情况下,第二个“a”只是在第二个处于范围内时覆盖了第一个,“a”。一旦它超出范围,第一个“a”就会重新出现。如果你没有意识到这一点,就可能导致微妙的错误!请注意,在Guy Coder的评论中进行澄清:当重新定义在某些let绑定(即在我的示例中的TestShadowing()函数内部)中时,我描述的行为发生。在实践中,我认为这是远远最常见的情况。但正如Guy所说,如果你在顶层重新定义,例如:
module Demo =

   let a = 1
   let a = 2

你确实会得到一个编译器错误。


你需要添加关于绑定不在let中而是在fs文件中完成会导致编译器错误的信息。 - Guy Coder

2

让我更直接地回答你问题的主要点,即重新绑定与突变之间的区别。您可以通过以下函数观察到区别:

let f () =
   let a = 1
   let g () = a
   let a = 2
   g () + a

该程序返回3,因为g中的a指向前一个绑定的a,而后者是独立的。上述程序与下面的程序完全等价:

let f () =
   let a = 1
   let g () = a
   let b = 2
   g () + b

我一直将第二个 a 及其所有引用重命名为 b


我可以在比较使用ref关键字时测试差异,但不能在使用mutable关键字时测试。 - user3056677

2
这种重新定义只在fsi中起作用。编译器会在此处产生错误,尽管偶尔可以做一些类似的事情。
let f h = match h with h::t -> h

当您创建一个新的h并遮盖参数中的定义时,它将返回第一个元素。

重新定义起作用的唯一原因是您可能会在fsi中犯错误,如下所示。

let one = 2;;
let one = 1;; //and fix the mistake

在编译的F#代码中,这是不可能的。

有趣的是,SML 允许阴影效应,这与突变不同。 - Craig Stuntz
1
所以 F# 也是如此。请看我的回答。 - Kit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接