I've mentioned in past messages that I had ported a suite of personal financial management tools successfully from C to D after first attempting the work with Rust. I thought I'd give you an example of one of the many headaches I encountered with Rust, because it's illustrative of my contention that Rust's memory- and thread-safety without a GC makes the programmer a much more active participant in the memory-management system than do languages equipped with a GC. It's a primary reason why Rust is so much more difficult to learn than languages with GC support.
I'm a Scheme enthusiast and have written a lot of it over my many years. A common pattern that is so easy to address in Scheme is a situation where a number of variables get set, usually at the head of a loop, and those variables (sometimes mutable, if they are, for example, Sqlite prepared statements) are needed by a number of functions that are part of the processing. Being able to define closures inside the loop that see those variables as part of their environment makes the code simpler, cleaner, and easier to write, as opposed to passing the needed variables as arguments to the functions at every calling site, C-style.
Rust has closures. Great. So here's an example of an attempt to do something along the lines described above with a single mutable variable:
fn main() {
let mut foo = 5;
let mut bar = || {
foo = 17;
};
let mut baz = || {
foo = 42;
};
bar();
println!("{}", &mut foo);
baz();
println!("{}", &mut foo);
}
I have a single mutable variable, foo, and two closures, bar and baz, that each mutate the variable. This solution isn't so bad, yes? Except it doesn't compile:
Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `foo` as mutable more than once at a time
--> src/main.rs:7:19
|
4 | let mut bar = || {
| -- first mutable borrow occurs here
5 | foo = 17;
| --- first borrow occurs due to use of `foo` in closure
6 | };
7 | let mut baz = || {
| ^^ second mutable borrow occurs here
8 | foo = 42;
| --- second borrow occurs due to use of `foo` in closure
9 | };
10 | bar();
| --- first borrow later used here
error[E0499]: cannot borrow `foo` as mutable more than once at a time
--> src/main.rs:11:20
|
7 | let mut baz = || {
| -- first mutable borrow occurs here
8 | foo = 42;
| --- first borrow occurs due to use of `foo` in closure
...
11 | println!("{}", &mut foo);
| ^^^^^^^^ second mutable borrow occurs here
12 | baz();
| --- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `playground` due to 2 previous errors
The problem is that the mutable borrows of foo in each of the closures occur, in effect, when the closure is defined, not when it is called. The compiler views this just as it would two assignments to foo -- two mutable borrows in the same scope -- and refuses to compile the program.
The solution is to use "interior mutability", which I view as a bit of a hack to get around the limitations of compile-time borrow-checking. Interior mutability lets you mutate variables that look immutable to the compiler and the safety of what you do is checked at runtime (so much for "zero cost"). Here's what the code looks like to fix the above using this approach:
use std::cell::RefCell;
fn main() {
let foo = RefCell::new(5);
let bar = || {
*foo.borrow_mut() = 17;
};
let baz = || {
*foo.borrow_mut() = 42;
};
bar();
println!("{}", foo.borrow());
baz();
println!("{}", foo.borrow());
}
This works, but at the cost of readability and some runtime efficiency.
The D equivalent:
import std.stdio;
void main()
{
int foo;
void bar() {
foo = 17;
}
void baz() {
foo = 42;
}
bar();
writeln(foo);
baz();
writeln(foo);
}
This is just one of multiple examples I personally encountered where Rust's approach to memory- and thread-safety makes life difficult for its users. On simple cost-benefit grounds, I can see using Rust for writing OS drivers (or even an entire OS) or for use in embedded systems, especially if there's a real-time constraint; in other words, where a GC would be unsuitable. But for ordinary applications on today's hardware? Use of Rust instead of D, Go, Nim, etc. makes no sense to me.