【问题标题】:What is the difference between mutable values and immutable value redefinition?可变值和不可变值重定义有什么区别?
【发布时间】:2013-12-17 22:39:55
【问题描述】:

我读到 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;;

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;;

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;;

【问题讨论】:

    标签: f# functional-programming immutability mutability


    【解决方案1】:

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

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

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

    【讨论】:

    • 伟大的理论概述..关于捕获名称的闭包,我想知道它如何影响 F# 中的可变值。让我测试一下
    【解决方案2】:

    '我不确定我是否同意给出的一些答案。

    以下代码在 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”就会重新出现。

    如果您没有意识到这一点,可能会导致细微的错误!

    根据 Guy Coder 的评论进行澄清:

    我上面描述的行为发生在重新定义在某些 let 绑定中(即在我的示例中的 TestShadowing() 函数中)。我想说这是迄今为止实践中最常见的情况。但正如盖伊所说,如果你在顶层重新定义,例如:

    module Demo =
    
       let a = 1
       let a = 2
    

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

    【讨论】:

    • 您需要添加有关绑定何时不在 let 中并在 fs 文件中完成的信息,这将导致编译器错误。
    【解决方案3】:

    这种重新定义只适用于 fsi。编译器会在此处产生错误,尽管您偶尔可以执行类似的操作

    let f h = match h with h::t -> h
    

    这将在您创建一个新的h 时返回第一个元素,它会从参数中隐藏定义。

    重新定义有效的唯一原因是您可能会像这样在 fsi 中犯错误

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

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

    【讨论】:

    【解决方案4】:

    让我为您的问题的要点添加一个更直接的答案,即重新绑定与突变有何不同。可以观察一下这个函数的区别:

    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 的使用情况进行比较
    猜你喜欢
    • 2011-10-27
    • 1970-01-01
    • 1970-01-01
    • 2012-10-10
    • 2019-05-29
    • 2019-02-03
    • 2016-10-19
    • 2014-09-28
    • 2019-10-02
    相关资源
    最近更新 更多