7

我试图弄清楚移动语义如何影响引用透明度。

引用透明性(RT) 允许我们用其结果替换任何表达式,而不会改变程序的含义(从Scala 中的函数式编程解释)。例如,我可以用 替换1 + 1我程序中的任何地方2,并且什么都不应该改变。这个 Python 程序是引用透明的:

@dataclass
class Bucket:
    things: List[str]

leaves = ["leaves"]

def bucket_with_sand(things: List[str]) -> Bucket:
    return Bucket(things + ["sand"])

bucket_with_sand(leaves)  # can be replaced with Bucket(["leaves", "sand"]) with no change to the program

而这个函数改变了它的论点

def bucket_with_sand(things: List[str]) -> Bucket:
    things += ["sand"]
    return Bucket(things)

因此用结果替换函数调用会改变含义。它不再是引用透明的。在像 Rust 那样具有移动语义的语言中,我们可以通过移动(并依赖于非-leaves的事实)来避免这个问题:VecCopy

struct Bucket {
    things: Vec<&str>,
}

let leaves = vec!["leaves"];

fn bucket_with_sand(things: Vec<&str>) -> Bucket {
    things.push("sand");
    Bucket { things }
}

bucket_with_sand(leaves); // mutates `things`
// doesn't matter that `leaves` has been mutated here as it's now out of scope

这似乎又是参照透明的。这个对吗?这些举措是否放松了对 RT 设计的传统限制?还是移动不是参照透明的?我特别想知道是否对 RT 有更广泛的影响,而我还没有看到。

4

1 回答 1

5

在几乎所有在真实计算机上执行的语言中,尤其是在具有命令式状态的语言中,引用透明性的概念都有些模糊,Rust 也不例外。调用可能会产生副作用——从执行 IO 到内存不足再到仅仅改变一个可变变量——并且取决于您是否将那些包含在“没有改变”的意义上的那些包括在内,您可能会认为函数是非参照透明。它们不是纯粹的数学函数,而是在调用时会改变世界状态的过程。

也就是说:Rust 所谓的“所有权”系统——它结合了“仿射”或“移动”类型及其多读者/单写者借用系统——有助于显着减少在一个程序。特别是它(大部分*)消除了大多数其他命令式语言中最普遍和最有害的副作用:可变别名。也就是说,在 Rust 中,您(大多数情况下*)永远不会有两个或多个对同一个内存位置的引用,其中一个函数中的一个引用会改变内存位置作为运行的副作用,而另一个函数中的另一个引用只是看到内存位置中的值“突然改变”。这意味着任何时候一个值将被改变,&mut或拥有变量——这意味着,正如您在这里所问的那样,在某种程度上,关于引用透明性的假设在 Rust 中比在大多数其他命令式语言中更有可能成为真的。

上面的“(mostly*)”星号指出了一个更大的例外:不安全的代码可能违反这条规则,并且在几个库函数中也是如此。例如,Rust 标准库中提供所谓的“内部可变性”的部分提供了一种不安全的单元类型以及以时间方式动态强制禁止可变别名的包装器类型:这样的可变访问可以在给定的情况下发生时间,但允许它们从不同的共享引用按顺序发生。

同样的警告适用于几乎所有真正的语言,无论它如何“纯粹”地推销自己:ML 系列有ref单元,Haskell 有其不安全的库函数,Lisps 有set!等等。这些都是对这样一个事实的让步,即有时能够通过数学抽象(函数式语言中的纯值,或 Rust 中的仿射值)到达具有不受限制的可变别名的底层机器具有压倒性的性能优势。

于 2020-06-18T23:39:41.293 回答