Skip to content

← Ownership II: Borrowing and the Borrow Checker step 16 of 24

Medium Primitives

split_at_mut and the disjointness-proof APIs

pub fn fold_halves(xs: &mut [i64]) -> usize

Fold the second half of the slice into the first: add each element of the second half into the matching element of the first half, then set that second-half element to zero. Return how many pairs were folded.

With n = xs.len() and half = n / 2 (integer division, rounding down):

  • the right half is the last half elements, xs[n - half ..];
  • the left half is everything before it, xs[.. n - half];
  • pair i, for i from 0 to half - 1, is xs[i] with xs[n - half + i];
  • when n is odd, the left half is one element longer. Its last element, index half (the exact middle), has no partner and keeps its value;
  • the return value is half.

Empty and one-element slices fold nothing and return 0. Values may be negative. The tests only use inputs where every sum fits in i64, so you do not need to handle overflow.

[1,2,3,4]    -> 2, [4,6,0,0]
[1,2,3,4,5]  -> 2, [5,7,3,0,0]     (index 2, value 3, is the untouched middle)
[10,20,30]   -> 1, [40,20,0]       (index 1, value 20, is the untouched middle)
[9]          -> 0, [9]
[]           -> 0, []

In the first example, 1 + 3 = 4 and 2 + 4 = 6, and the right half becomes zeros. In the second, 1 + 4 = 5 and 2 + 5 = 7, and the middle 3 is left alone.

Why the starter does not compile

The starter holds two mutable references into the slice at the same time:

let dst = &mut xs[i];
let source = &mut xs[n - half + i];
*dst += *source;

The compiler rejects it with E0499 ("cannot borrow xs[_] as mutable more than once at a time"). The _ in that message shows how the borrow checker sees an indexed place: it does not compare index values, so it treats any two xs[...] borrows as possibly the same element, even though i and n - half + i are different positions here. The diagnostic's help line suggests split_at_mut, the method this exercise is about. (Item 3.7 covers this error in more detail.)

A function whose signature promises disjointness

The standard library provides a method on slices for exactly this situation:

pub fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T])

It takes one mutable borrow of the slice and returns two mutable slices: [0, mid) and [mid, len). It panics if mid > len. Both results borrow from the original slice, so the original cannot be used while either is still in use.

The two output slices never overlap. The caller's borrow check relies on the signature, not on that fact: at a call site the compiler checks your code against the types of the function, and it does not inspect the function's body. Inside the standard library, the implementation uses unsafe code to build the two slices, and its authors are responsible for showing that they cannot overlap. Callers get a safe API and write no unsafe themselves.

This is a useful pattern to look for when the borrow checker rejects code that you know touches separate data. Before reaching for unsafe or a runtime-checked type such as RefCell, check whether a standard-library method already returns the separate borrows you need.

Related slice methods

Each of these returns mutable access to parts of a slice that do not overlap:

method what it gives you
split_at_mut(mid) two halves; panics if mid > len
split_at_mut_checked(mid) the same, returning None instead of panicking (stable since Rust 1.80)
split_first_mut() / split_last_mut() one element and the rest, or None for an empty slice
chunks_mut(size) / chunks_exact_mut(size) consecutive blocks of size elements; panics if size is 0
rchunks_mut(size) blocks counted from the end
iter_mut() a mutable reference to each element in turn
get_disjoint_mut([i, j]) references at arbitrary indices, returning an error if an index is out of bounds or repeated (stable since Rust 1.86)

More than one of these can solve this problem. get_disjoint_mut can fetch each pair separately. chunks_mut(n - half) yields the left half and then the right half, but it panics on an empty slice because the chunk size would be 0. split_at_mut is the most direct fit.

Choosing the split point

Split at n - half, not at half. With xs.split_at_mut(n - half), the middle element of an odd-length slice ends up at the end of the left slice. With split_at_mut(half), it ends up at the start of the right slice instead, and the wrong pairs are folded: [1,2,3,4,5] would become [4,6,0,0,5].

After splitting, left.iter_mut().zip(right.iter_mut()) walks the pairs. zip stops when the shorter iterator ends. The right half is never longer than the left, so the middle element is skipped without a special case.

Grading

The submission must compile, pass the tests and produce no warnings under cargo clippy -- -D warnings. A plain index loop such as xs[i] += xs[n - half + i] also compiles, because each index expression borrows the slice only briefly and no two mutable references are held at once. The exercise is aimed at the split-based approach, but the tests check only the result.