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