Last active
September 9, 2024 16:07
-
-
Save redbar0n/e5a855fa0d7b75756dc4acfc94ffafb2 to your computer and use it in GitHub Desktop.
Rust - ownership/borrowing could have been simpler without "mutable borrows"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// "Mutable borrows" is where the Rust ownership/borrowing model became too complex for it's own good... | |
// A function that mutates a value, but does not return the value. So it only performs an effect, often called a side-effect. | |
fn mutate_value(s: &mut String) { | |
s.push_str(", world"); // we can mutate the borrowed value. In Rust, push_str returns the unit value, aka. void, so the function does not return the string. | |
} | |
fn main() { | |
let mut hello = String::from("hello"); | |
// ... potentially many lines of code | |
mutate_value(&mut hello); // hello is mutably borrowed, so we can still use it after the call | |
// ... potentially many lines of code | |
println!("{}", hello); // prints "hello, world" -- the hello variable name here actually lies, since if you just read the definition of hello it has changed/mutated in the meanwhile, and you don't know immediately what it has mutated to, without inspecting all interceding lines and function calls and their docs or internals. Calling the variable X wouldn't have been more helpful. A name should refer to the state of a data structure, and a data structure in a particular state should have a particular name, so it is predictable. It's not the same as saying always use immutable constants instead of variables, because we do want mutability, but we also want to discern the mutated results from the original. A little bit of re-naming thus goes a long way for clarity. | |
} | |
// But why not simply pass ownership forth and back instead? So called move-and-return ownership? | |
fn takes_ownership(s: String) { | |
s.push_str(", world"); // we can mutate the moved value | |
} | |
fn main() { | |
let hello = String::from("hello"); | |
// ... potentially many lines of code | |
let hello_world = takes_ownership(hello); // hello is moved, so we can't use it anymore, but we can return it mutated as hello_world | |
// ... potentially many lines of code | |
println!("{}", hello_world); // prints "hello, world" -- since hello was mutated it should be reflected in a new variable name as it is here. It is clearer, plus, by passinng ownership forth and back like this, you could get rid of the whole notion and syntax of "mutable borrows". | |
} | |
// It seems to be just as efficient, as copying references (or primitive values), as it does when transferring ownership, is negligible: https://www.perplexity.ai/search/when-is-something-sm3oOZD3SlyzpsQNlASAww | |
// Using move-and-return semantics in Rust, the values are moved by reference, not by copy, unless the data type has been explicitly marked as Copy. | |
// Yes, it is difficult to achieve this now in Rust, since many functions and libraries in the ecosystem are | |
// already made based on such side-effectful functions that mutate values without also returning them. | |
// But if Rust had been designed to enforce functions always returning values, as an _enforced_ expression based language, | |
// then its ownership/borrowing model could have been conceptually simpler. But alas, Rust is irrevocably imperative and side-effectful. | |
// After all, borrowed mutability for values is 'shared mutable state', which is everything we want to avoid. | |
// It's why Rust ensures you can only have 1 mutable borrow at a time: https://medium.com/@Faisalbin/a-tale-of-rust-ownership-and-multiple-mutable-borrows-2858375b92f4 | |
// But even then, it shares the value/state between the function borrowing the mutable reference, and the calling code, | |
// which introduces lies further down in the scope of the calling code. | |
// By simply returning the new mutated state, and naming it, it could have been avoided. We also don't want to rely on side-effects, even if they are made more explicit. | |
// Passing around ownership like suggested above could be a practise to employ in one's own code, to avoid anyone having to think about 'mutable borrows', or being misled by variable names. | |
// To always return the mutated value, maybe you could take third party side-effectful library functions and wrap them in your own functions (that always return the mutated value)? | |
// So that your own code could avoid using "mutable borrows" and only rely on ownership passing for mutation. | |
// third party library function: | |
fn mutate_value(s: &mut String) { | |
s.push_str(", world"); | |
} | |
fn wrapped_takes_ownership(s: String) -> String { | |
mutate_value(&mut s); | |
s; // Rust automatically returns the last value implicitly | |
} | |
fn main() { | |
let hello = String::from("hello"); | |
// ... potentially many lines of code | |
let hello_world = wrapped_takes_ownership(hello); | |
// ... potentially many lines of code | |
println!("{}", hello_world); | |
} | |
// But it's likely not worth the hassle at this point... It would take a lot of work to maintain and enforce, and Rust is already pretty complex, so learning to deal with 'mutable borrows' where you absolutely have to, is probably the easier approach.. | |
// However, this overall point could be worth noting to language designers who want to take inspiration from Rust's ownership/borrowing model. | |
// So it could be designed less complex from the start. | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment