Iceblaze
Iceblaze

Reputation: 73

Is Rust's borrow checker overly complicating this slices example?

I am currently exploring the wonders of Rust by rewriting exercises from A Tour of Go.

I understand both Go and Rust have different features, not everything is fully rewritable and I had my share of fighting the borrow checker. However I got to one fairly simple exercise, yet all solutions I come up with seem very... complex.

The Go Example

package main

import "fmt"

func main() {
  names := [4]string{
    "John",
    "Paul",
    "George",
    "Ringo",
  }
  fmt.Println(names) // [John Paul George Ringo]

  a := names[0:2]
  b := names[1:3]
  fmt.Println(a, b) // [John Paul] [Paul George]

  b[0] = "XXX"
  fmt.Println(a, b) // [John XXX] [XXX George]
  fmt.Println(names) // [John XXX George Ringo]
}

In Go we just create 2 slices do a mutation through one and we are done. We do some tradeoff of safety for simplicity thanks to the GC.

The Rust Example - #1

fn main() {
    let mut names = ["John", "Paul", "George", "Ringo"];
    println!("{:?}", names); // [John Paul George Ringo]

    {
        let a = &names[..2];
        let b = &names[1..3];
        println!("{:?} {:?}", a, b); // [John Paul] [Paul George]
    }

    {
        // need a separate mutable slice identical to 'b'
        let tmp = &mut names[1..3];
        tmp[0] = "XXX";
    }

    {
        // need to assign same variables just to print them out
        let a = &names[..2];
        let b = &names[1..3];
        println!("{:?} {:?}", a, b); // [John XXX] [XXX George]
    }

    println!("{:?}", names); // [John XXX George Ringo]
}

This is as close to a one to one rewrite of the previous example as I can get, obviously this is far from optimal due to extra duplicity and overhead involved, so I created a second example.

The Rust Example - #2

fn slice_writer(arr: &[&str]) {
    let a = &arr[..2];
    let b = &arr[1..3];
    println!("{:?} {:?}", a, b);
}

fn main() {
    let mut names = ["John", "Paul", "George", "Ringo"];
    println!("{:?}", names);

    slice_writer(&names);

    {
        // still need to have the duplicity of '[1..3]'
        let tmp = &mut names[1..3];
        tmp[0] = "XXX";
    }

    slice_writer(&names);

    println!("{:?}", names);
}

This feels really cumbersome to write; I need to create a separate function just to remove duplicity of assigning the same slices, a problem I shouldn't have in the first place. Rust creates all these safety measures but it either causes a degradation of performance as we need to create those same variables multiple times, clear them, hold the function in memory, etc. or I need to use some esoteric 'unsafe' procedures and what is the point of using the borrow checker then?

Summary

Am I missing something obvious here? What is the simple solution to this problem? Or is this how it is supposed to be done? In that case I can't imagine what it will be like writing something more massive than a single slice mutating program.

Upvotes: 2

Views: 234

Answers (1)

the8472
the8472

Reputation: 43052

The Go example is simply not safe. It performs a mutation on aliased memory. If you moved those slices to different threads you could see data races.

Which means the Go compiler cannot perform noalias based optimizations. On the other hand the borrow checker in Rust ensure that mutable pointers are not aliased.

Rust creates all these safety measures but it either causes a degradation of performance as we need to create those same variables multiple times, clear them, hold the function in memory, etc.

Have you actually observed such degradation or compared optimized compiler output?

Upvotes: 4

Related Questions