Borrow and Inout Parameters
When you read code, you want to understand what it does without tracing through every function. Rue helps by making mutation visible at the call site.
This chapter builds on ownership from the structs chapter. Passing a non-@copy struct by value moves it into the callee. borrow and inout are how you let a function access a value without taking ownership of it.
Reading Code at a Glance
Look at this code:
fn main() -> i32 {
let mut values = [10, 20, 30];
double_all(inout values);
values[0]
}
fn double_all(inout arr: [i32; 3]) {
let mut i: u64 = 0;
while i < 3 {
arr[i] = arr[i] * 2;
i = i + 1;
}
}
Without seeing double_all's definition, you already know it modifies values. The inout keyword tells you: this function will change my data.
Compare that to languages where mutation is invisible:
// Go: does this modify values? You can't tell without reading sort()
sort.Ints(values)
# Python: same problem
In Rue, mutation is always explicit at the call site.
How It Works
Inout: Modify in Place
Use inout when a function needs to modify its argument:
fn double_all(inout arr: [i32; 3]) {
let mut i: u64 = 0;
while i < 3 {
arr[i] = arr[i] * 2;
i = i + 1;
}
}
fn main() -> i32 {
let mut values = [10, 20, 30];
double_all(inout values);
println(@to_string(values[0])); // prints: 20
println(@to_string(values[1])); // prints: 40
println(@to_string(values[2])); // prints: 60
values[0]
}
Both the function signature and the call site use inout. There's no way to accidentally miss that mutation is happening.
Borrow: Read Without Copying
Use borrow when you want to read data without copying it:
fn sum_array(borrow arr: [i32; 5]) -> i32 {
let mut total = 0;
let mut i: u64 = 0;
while i < 5 {
total = total + arr[i];
i = i + 1;
}
total
}
fn main() -> i32 {
let numbers = [1, 2, 3, 4, 5];
let sum = sum_array(borrow numbers);
println(@to_string(sum)); // prints: 15
sum
}
With borrow, you know the function won't change your data.
Borrowing also keeps non-copy structs usable after the call:
struct Point {
x: i32,
y: i32,
}
fn sum_point(borrow p: Point) -> i32 {
p.x + p.y
}
fn main() -> i32 {
let p = Point { x: 10, y: 20 };
let sum = sum_point(borrow p);
println(@to_string(sum)); // prints: 30
println(@to_string(p.x)); // still valid: p was borrowed, not moved
sum
}
Combining Them
You can mix borrow and inout in a single function:
fn copy_into(borrow src: [i32; 3], inout dst: [i32; 3]) {
let mut i: u64 = 0;
while i < 3 {
dst[i] = src[i];
i = i + 1;
}
}
fn main() -> i32 {
let source = [1, 2, 3];
let mut dest = [0, 0, 0];
copy_into(borrow source, inout dest);
println(@to_string(dest[0])); // prints: 1
println(@to_string(dest[1])); // prints: 2
println(@to_string(dest[2])); // prints: 3
0
}
Reading the call site, you immediately know: source is read, dest is modified.
Rue enforces the same rule inside one call: any number of borrow accesses, or one inout access, but not both for the same value at the same time.
fn observe(borrow old: i32, inout new: i32) -> i32 {
old + new
}
fn main() -> i32 {
let mut x = 1;
// ERROR: cannot borrow x while it is also passed as inout
// observe(borrow x, inout x);
0
}
Uncommenting the observe call makes the example invalid. The compiler rejects it because the callee would have both read-only and mutable access to x during the same call.
With Structs
These work with any type:
struct Point {
x: i32,
y: i32,
}
fn translate(inout p: Point, dx: i32, dy: i32) {
p.x = p.x + dx;
p.y = p.y + dy;
}
fn print_point(borrow p: Point) {
println(@to_string(p.x));
println(@to_string(p.y));
}
fn main() -> i32 {
let mut pos = Point { x: 10, y: 20 };
print_point(borrow pos); // prints: 10, 20
translate(inout pos, 5, -3);
print_point(borrow pos); // prints: 15, 17
0
}
Destructors and Early Drop
Some move-only values need cleanup when they go out of scope. Define that cleanup with a drop fn named after the struct:
struct Guard {
value: i32,
}
drop fn Guard(self) {
println(@to_string(self.value));
}
fn main() -> i32 {
let _guard = Guard { value: 42 };
println(@to_string(1));
0
} // prints: 42 when _guard is dropped
You can also drop a value early with @drop. Dropping consumes the value, so it cannot be used afterward:
struct Guard {
value: i32,
}
drop fn Guard(self) {
println(@to_string(self.value));
}
fn main() -> i32 {
let guard = Guard { value: 42 };
@drop(guard); // prints: 42 here
// guard.value // ERROR: guard was moved into @drop
0
}
A type with a destructor represents ownership of cleanup work, so it should stay move-only. Do not mark destructor-bearing types with @copy.