2
votes

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.

1
Hi there! Could you clarify what you are actually asking? If you want to know the best way to translate the Go example and want to get comments on your solutions, you should rather ask on codereview.SE. If you want to ask something about the borrow checker, you should (1.) edit your title to reflect that, and (2.) make sure your question is focused: questions like "what's the point of the borrow checker anyway?" are usually way too broad for StackOverflow. Thanks! - Lukas Kalbertodt
"We do some tradeoff of safety for simplicity thanks to the GC." - how is Go's GC involved in this example? - the8472
a degradation of performance as we need to create those same variables multiple times — that's not really how compiled / statically typed languages with optimizing compilers work. Performance can only be decided at the bulk algorithmic level (e.g. O(N) vs O(N^2)) or via measurement. clear them — I don't think there's any such activity here. hold the function in memory — it's likely the function is inlined, but the memory difference between the two will be negligible. - Shepmaster
What is the simple solution to this problem — I'd probably write it like this. writing something more massive than a single slice mutating program — it's very rare, in practice, that you deliberately want to spread out overlapping mutable access, so Rust helps keep you from losing track of all your mutability. - Shepmaster
is this how it is supposed to be done — yes, one of Rust's core tenets is that there can only be one thing at a time that is allowed to mutate a value. Amusingly, this is even advocated for in a book about programming in Go and goes by the same phrase: mutability xor aliasing - Shepmaster

1 Answers

4
votes

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?