A D vs. Rust example
Don Allen
donaldcallen at gmail.com
Thu Oct 20 13:37:07 UTC 2022
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.
More information about the Digitalmars-d
mailing list