3

I am trying to implement the quick sort algorithm for an array of i32. My question is about an apparent inconsistency in the use of &mut when passing in arrays to functions. My code:

fn main() {
    let mut my_array = [5, 2, 8, 7];
    let high = my_array.len() - 1;
    quick_sort(&mut my_array, 0, high);
    print!("{:?}", my_array);
}

fn quick_sort(array: &mut [i32], low: usize, high: usize) {
    let pivot_idx = partition(array, low, high);

    // Separately sort elements before partition and after partition
    if pivot_idx > low {
        quick_sort(array, low, pivot_idx - 1);
    }
    if pivot_idx < high {
        quick_sort(array, pivot_idx + 1, high);
    }
}

fn partition(array: &mut [i32], low: usize, high: usize) -> usize {
    let pivot = array[high]; // pivot
    let mut i = low;

    for j in low..high {
        // If current element is less than or equal to pivot
        if array[j] <= pivot {
            let swapper_variable = array[i];
            array[i] = array[j];
            array[j] = swapper_variable;
            i += 1; // increment index of smaller element
        }
    }

    let swapper_variable = array[i];
    array[i] = array[high];
    array[high] = swapper_variable;
    return i;
}

My code works, but should it? In main I call quick_sort with quick_sort(&mut my_array, 0, high), but the recursive call within quick_sort is quick_sort(array, low, pivot_idx - 1), without the &mut.

Furthermore, also within quick_sort, I call partition with partition(array, low, high), but partition takes a mutable array as an argument and I haven't specified &mut in the call! If I do try to put &mut in the partition function call, the code will not compile: error[E0596]: cannot borrow `array` as mutable, as it is not declared as mutable --> src/main.rs:9:31 | 8 | fn quick_sort(array: &mut [i32], low: usize, high: usize) { | ----- help: consider changing this to be mutable: `mut array` 9 | let pivot_idx = partition(&mut array, low, high); | ^^^^^^^^^^ cannot borrow as mutable

So it seems there's some confusion as to what's mutable, the array itself or the pointer(?) to the array. But it seems to me that I should be able to call functions within quick_sort in the same manner that I called quick_sort originally from main. As far as I know, the point of forcing &mut to be explicitly placed in the function call is that doing so makes it easy to see that the called function might modify the argument, but the calls within quick_sort give no indication that they modify the array.

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=62813cd1bd23d8b1a36d6126d4305800

Gabe Nodarse
  • 182
  • 7

1 Answers1

4

In your main function, my_array is of type [i32], but quick_sort requires &mut [i32], which is why you need &mut my_array; In your quick_sort function, however parameter array is already of type &mut [i32], which is why you can directly pass it to partition and recursive quick_sort call. This is exactly how you should call your functions following the type annotations.

fn main() {
    let mut my_array = [5, 2, 8, 7];         // [i32]
    quick_sort(&mut my_array, 0, high);      // &mut my_array gives type &mut [i32]
}

fn quick_sort(array: &mut [i32], low: usize, high: usize) {
    let pivot_idx = partition(array, low, high);    
    // notice array is already of type &mut [i32], which is required by partition function,
    // you shouldn't take another reference on top of it
    ...
}
Psidom
  • 171,477
  • 20
  • 249
  • 286
  • 2
    Just to be picky: in `main` the type of `my_array` is `[i32; 4]`, `&mut my_array` is then `&mut [i32; 4]` which coerces to `&mut [i32]`. – mcarton Dec 24 '19 at 22:41
  • Okay, thinking of &mut [i32] as a distinct type helps me justify it in my mind. I'm loving Rust so far, I just find the mix of magic and non-magic unnecessarily confusing. Why can I call the dot operator on a borrowed type, but for passing it into a function I have to treat it as different? I wish that borrowed values were treated like the originals, with the borrowed qualifier only meaning it wasn't copied and won't expire in that scope. Then there could be a keyword/typecast for accessing the pointer itself. Wouldn't that be more intuitive than this half-assed abstraction away of pointers? – Gabe Nodarse Dec 26 '19 at 09:06
  • *call the dot operator on a borrowed type* and *pass it into a function*. Well, you mentioned two operations, but you haven't really defined them well. Basically the second operation is more general which applies to any static language, i.e. you should consider types always. The reason you can *call the dot operator on a borrowed type* is a special case, which is due to rust's auto dereference: https://stackoverflow.com/questions/28519997/what-are-rusts-exact-auto-dereferencing-rules. – Psidom Dec 26 '19 at 17:26
  • *I wish that borrowed values were treated like the originals*. I think auto derefence to some extent is trying to achieve that purpose from the usage stand point. But that doesn't mean we can ignore the difference, as that would defeat all the purposes of the borrow-checker system that rust enforces, wouldn't it? – Psidom Dec 26 '19 at 17:42