Initialization
A var may be declared before its value is known, and given one later. Until then it holds nothing, and the compiler tracks it: every path that reaches a read must have assigned it first. A read that some path can reach without an assignment is rejected at compile time — so a Rux program never sees leftover garbage from an unassigned variable.
This is the last lesson of the part, and it closes the circle on ownership. A value's life ends with its destructor; this lesson is about how it begins, and why the compiler must always know whether a variable holds a value at all.
Declare now, assign later
Sometimes the value depends on a decision that has not been made yet. grade is declared with a type and no value, and each branch of an else if chain assigns it:
func Grade(score: int32) -> char8[..] {
var grade: char8[..];
if score >= 90 {
grade = "excellent";
} else if score >= 50 {
grade = "pass";
} else {
grade = "retry";
}
return grade;
}
The compiler follows every path from the declaration to the return, and on each one grade has been assigned. The read is accepted.
flowchart LR
d["var grade: char8[..]<br/>holds nothing"] --> q{"score?"}
q -- ">= 90" --> a1["grade = excellent"]
q -- ">= 50" --> a2["grade = pass"]
q -- "else" --> a3["grade = retry"]
a1 --> r["return grade<br/>assigned on every path"]
a2 --> r
a3 --> r
q -. "without the final else" .-> x["error: value 'grade' may be<br/>uninitialized on some<br/>control-flow paths"]Delete the final else, and a score below 50 reaches the return with nothing assigned. The compiler refuses the program and points at where that path starts.
Reading too early
The simplest case is a read straight after the declaration:
var total: int32;
PrintLine("{}", total);
error: variable 'total' is used before it is initialized
note: 'total' was declared without an initializer at 47:5
help: assign a value to 'total' before this use
Many languages would print whatever happened to be in that memory, or a silent zero. Rux makes you say what the value is.
A let needs its value now
A let has no such freedom. It can never be assigned later, so it needs its value where it is declared: let total: int32; on its own is rejected with error: immutable variable requires an initializer.
One field at a time
A plain struct of numbers may be filled in one field at a time, and it counts as assigned once every field has been written. The compiler tracks the fields individually: after var p: Point; p.x = 1;, reading p.x is fine, but reading p.y is refused, with a note that only p.x has been written.
A type that needs destroying is different. When a struct holds a Guest — the type with a destructor from the Destructor lesson — a guest written into one field of an empty struct would belong to no whole value, and nothing would ever destroy it. So such a variable must get its value whole:
var table: Table;
table.guest = Guest { name: "Gus" };
error: cannot write field 'guest' of 'table', which holds no value
help: initialize 'table' whole, as in 'table = Table { ... }'
It is the same rule as in Partial move, seen from the other end: a value with a destructor is alive whole, or not at all.
A constructor runs for you
There is one exception to "holds nothing". When the type has a constructor that takes no arguments, var value: T; calls it, and the variable starts out initialized:
extend Tally {
func Tally() -> Tally {
PrintLine(" Tally() starts the count at zero");
return Tally { count: 0 };
}
}
var tally: Tally;
tally.count += 1;
tally.count += 1 reads the count before changing it, and that read is fine — the constructor has already run, as the output shows.
| Declaration | Starts as |
|---|---|
var grade: char8[..]; | nothing — assign before reading |
var tally: Tally; with Tally() | whatever Tally() returns |
let total: int32; | an error — a let needs its value |
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A `var` may be declared before its value is known, and given one later. Until then it holds
// nothing, and the compiler tracks it: every path that reaches a read must have assigned it
// first. A read that some path can reach without an assignment is rejected at compile time, so
// a Rux program never sees leftover garbage from an unassigned variable.
//
// A `let` has no such freedom. It cannot be assigned later, so it needs its value where it is
// declared.
//
// One exception to "holds nothing": when the type has a constructor taking no arguments,
// `var value: T;` calls it, and the variable starts out initialized.
import Io::PrintLine;
// Every branch assigns `grade`, so the read in `return` is accepted. Delete the final `else` and
// one path reaches the read with nothing assigned:
//
// error: value 'grade' may be uninitialized on some control-flow paths
// note: one unavailable path for 'grade' originates at 20:5
// help: initialize or preserve 'grade' on every path before this use
func Grade(score: int32) -> char8[..] {
var grade: char8[..];
if score >= 90 {
grade = "excellent";
} else if score >= 50 {
grade = "pass";
} else {
grade = "retry";
}
return grade;
}
struct Tally {
count: int32;
}
extend Tally {
func Tally() -> Tally {
PrintLine(" Tally() starts the count at zero");
return Tally { count: 0 };
}
}
func Main() -> int {
PrintLine("grades: {}, {}, {}", Grade(95), Grade(72), Grade(31));
// Declared without a value and read straight away:
//
// var total: int32;
// PrintLine("{}", total);
//
// error: variable 'total' is used before it is initialized
// note: 'total' was declared without an initializer at 47:5
// help: assign a value to 'total' before this use
// When the type needs destroying, a variable without a value must also get its value whole.
// Here `Table` is a struct holding a `Guest`, the type with a destructor from the Destructor
// lesson. A guest written into one field would belong to no whole table, and nothing would
// ever destroy it:
//
// var table: Table;
// table.guest = Guest { name: "Gus" };
//
// error: cannot write field 'guest' of 'table', which holds no value
// help: initialize 'table' whole, as in 'table = Table { ... }'
// `Tally` has a constructor without arguments, so this declaration runs it.
PrintLine("tally:");
var tally: Tally;
tally.count += 1;
PrintLine(" count {}", tally.count);
return 0;
}
Run it
cd Examples/Ownership/Initialization
rux run
grades: excellent, pass, retry
tally:
Tally() starts the count at zero
count 1
Common mistakes
Without the final
else in Grade, return grade; fails with error: value 'grade' may be uninitialized on some control-flow paths. Every branch that can reach the read must assign — add the missing else, or give the variable a sensible starting value.var total: int32; PrintLine("{}", total); fails with error: variable 'total' is used before it is initialized. There is no hidden zero. If zero is what you mean, write var total: int32 = 0;.let without a value.let total: int32; fails with error: immutable variable requires an initializer. Declare it with var if it must be assigned later, or give it its value on the spot.var table: Table; table.guest = Guest { name: "Gus" }; fails with error: cannot write field 'guest' of 'table', which holds no value. Assign the whole struct: table = Table { guest: Guest { name: "Gus" } };.Try it yourself
- Delete the final
elseinGradeand read the error and its note. - Declare
struct Point { x: int32; y: int32; }, thenvar p: Point; p.x = 1;, and printp.y. Then assignp.ytoo and print both. - Remove the
Tally()constructor. Which lines inMainare now rejected, and what would you write instead?
Learn more
- Constructor — the function
var value: T;calls - Mutable —
varandlet varandletin the Rux Reference
11.9 Defer return
See that return keeps its value before deferred code runs, and use that to hand out a value and then advance.
Overview
Behaviour that many types share: declare an interface, hold any implementing type in one value, and plug your own types into printing, comparison, operators, indexing and for loops.