可变值和不可变值重定义之间有什么区别?

时间:2013-12-02 09:16:29

标签: f# functional-programming immutability mutability

我已经读过F#中的值是不可变的。但是,我也遇到了重新定义值定义的概念,它影响了以前的定义。这与可变值有什么不同?我这不仅仅是作为理论构造,而且还有关于何时使用可变值以及何时重新定义表达式的建议;或者如果有人能够指出后者不是惯用的f#。

重新定义的基本示例:

let a = 1;;
a;; //1
let a = 2;;
a;; //2

更新1:

添加到下面的答案中,顶级Fsharp交互式中的重新定义仅允许在不同的终端中。以下将在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;;

除了使用闭包的不同之外,我无法确定重新定义和使用mutable关键字之间的实际区别,例如:

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 个答案:

答案 0 :(得分:5)

我对F#并不熟悉,但我可以回答“理论”部分。

对象的变异是(或至少有可能)全局可见的副作用。任何其他引用同一对象的代码都将观察到更改。现在可以更改在程序中任何位置依赖于对象值的任何属性。例如,如果以影响其排序位置的方式改变该列表中引用的对象,则列表已排序的事实可能会呈现为false。这可能是一个非常不明显和非本地的效果 - 处理排序列表的代码和执行变异的代码可能位于完全独立的库中(既不直接依赖另一个),只通过长链调用连接(其中一些可能是其他代码设置的闭包)。如果你使用相当广泛的变异,那么两个位置之间甚至可能没有直接的调用链接,事实上这个可变对象最终被传递给 变异代码可能依赖于到目前为止程序执行的特定操作顺序。

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

答案 1 :(得分: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()函数内)。我想说这是迄今为止最常见的情况。但正如盖伊所说,如果你重新定义在最高层,例如:

module Demo =

   let a = 1
   let a = 2

你的确会遇到编译错误。

答案 2 :(得分:2)

这种重新定义只适用于fsi。编译器会在这里产生错误,尽管你偶尔可以做一些像

这样的事情
let f h = match h with h::t -> h

将在您创建新的h时返回第一个元素,该新的let one = 2;; let one = 1;; //and fix the mistake 会从参数中隐藏定义。

redifintion工作的唯一原因是你可能会在fsi中犯错误

{{1}}

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

答案 3 :(得分:2)

让我更直接地回答你问题的主要观点,即重新绑定与突变的区别。您可以观察此功能的不同之处:

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

返回3,因为a中的g指的是a的前绑定,而后者是分开的。上述程序完全等同于

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

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