I'm trying to work out how move semantics affect referential transparency.
Referential transparency (RT) allows us to replace any expression with its result without changing the meaning of the program (paraphrased from Functional Programming in Scala). For example, I can replace 1 + 1
anywhere in my program with 2
, and nothing should change. This Python program is referentially transparent:
@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
whereas this function mutates its argument in place
def bucket_with_sand(things: List[str]) -> Bucket:
things += ["sand"]
return Bucket(things)
so replacing the function call with its result changes the meaning. It's no longer referentially transparent. In a language with move semantics like Rust's, we can avoid this problem by moving leaves
(and relying on the fact that Vec
is non-Copy
):
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
This appears to be referentially transparent again. Is this correct? Do such moves relax conventional constraints on RT design? Or are moves not referentially transparent? I'm particularly interested to know if there are there broader implications on RT that I've not seen.