This topic was discussed few times in past. Probably some of the notes written below are wrong or sub-optimal.
&T and T function arguments are “in”.
&mut function arguments are “inout”.
So “out” arguments are missing.
In a function a &out argument “x: &out u32” is like an unintialized reference variable:
fn foo(x: &out u32) {
println!("{}", x); // Error, x is not initialized yet.
}
fn foo(data: &out [u32; 5]) {
data = [0; 5]; // OK.
// Optionally use data here.
}
trait Foo {
fn bar(&out self); // OK.
}
But are “out” arguments common enough in Rust to justify this type system feature (and increased complexity)? Rust has tuple return types that avoid some of the usages of out arguments in other languages:
fn foo(x: u32) -> (u32, u64);
An example usage. Given a simple helper function:
#[inline]
fn fill_u8(data: &out [u8], value: u8) {
unsafe {
std::ptr::write_bytes(data.as_mut_ptr(), value, data.len());
}
}
User code like this:
arr[0] = b'a';
fill_u8(&out arr, b'0');
Gives a warning like:
warning: value assigned to
arr[0]is never read
It’s a situation like:
fn main() {
let mut x;
x = 1;
x = 2;
println!("{}", x);
}
Statically verifying that code like this is correct is not obvious:
fn foo(v: &out Vec<usize>) {
for (i, el) in v.iter_mut().enumerate() {
*el = i;
}
}
fn foo(v: &out [usize]) {
for i in 0 .. v.len() {
*v[i] = i;
}
}
Here v.len() is read before it’s written.
Even if you add new features to a type system there are usually other coding patterns left that the type system can’t model (well enough) yet. This a common Rust code pattern, to reduce the number of heap allocations buffers are given to a function that’s called many times. The contents of the buffers are ignored, just their capacity is important. It’s kind of opposite of “out”:
fn foo(buffer1: &mut Vec<u32>, buffer2: &mut HashMap<u32, u32>) {
buffer1.clear(); // Implicit at entry?
buffer2.clear(); // Implicit at entry?
// ... Uses buffer1 and buffer2 here.
}
D language has “out” arguments too, its semantics is different from the one explained above:
import std.stdio;
void foo(out uint[] v2, out uint[3] a2) {
writeln("B: ", v2, " ", a2);
}
void main() {
uint[] v1 = [1, 2, 3]; // Heap-allocated.
uint[3] a1 = [10, 20, 30];
writeln("A: ", v1, " ", a1);
foo(v1, a1);
writeln("C: ", v1, " ", a1);
}
Outputs:
A: [1, 2, 3] [10, 20, 30]
B: [] [0, 0, 0]
C: [] [0, 0, 0]
So dynamic arrays get their lenght set to zero, and fixed-size arrays get zeroed out at the beginning of the foo() function. This costs more run-time work compared to the design above (that just tags an out reference variable as uninitialized and requires the programmer to initialize it) but it makes both the compiler and the semantics simpler.
The Ada design of “out” arguments instead is more similar to the Rust ideas shown above: https://en.wikibooks.org/wiki/Ada_Programming/Subprograms
This is another more niche pattern not covered by Rust “&out”, given:
fn perm swap(&mut self, i: usize, j: usize) {...}
A function like this:
fn shuffle(data: &mut [u32], rng: &mut XorShift128) {
for i in (1 .. data.len() - 1).rev() {
data.swap(i, rng.uniform(0, i));
}
}
Could be annotated like:
fn shuffle(data: &perm [u32], rng: &mut XorShift128) {
&perm means that you can read data freely, but you can’t write data items directly, you can only swap them, so after sort the items of data are the same, just in a different position. Swap-based sorting and permutating functions are natural examples of this not too much common coding pattern. At this level of specificity probably it’s better to use dependent typing, liquid typing, or code proofs instead.