Null & Initialization
Stof has null, but no separate "undefined" — a field that doesn't exist and a field explicitly set to null look the same from the outside. That keeps the model simple, at the cost of losing that particular distinction some languages make.
Not-Null Types
A ! suffix on a type makes a field, variable, parameter, or return type reject null outright — Fields touched on this briefly; here's what the rejection actually looks like:
#[main]
fn main() {
let value: int! = 42;
try { value = null; }
catch { pln('blocked: value can never be null'); }
pln(value);
}Null Checks
The usual tools work as expected — == null, and any falsy check via !value:
#[main]
fn main() {
let value = null;
pln(value == null, !value);
const fallback = value ?? 42;
pln(fallback);
}?? is shorthand for "use the left side unless it's null, then use the right" — lhs ?? rhs.
Field Paths Are Always Null-Safe
Reading a field by path never throws, even if part of the path doesn't exist — it just returns null, which makes ?? chains a natural fit for defaults:
#[main]
fn main() {
const field_val = self.field.another.value ?? self.field.other ?? "default";
pln(field_val);
}None of field, another, or other exist on this document — the chain still resolves to "default" instead of throwing.
The ? Prefix Operator
Field paths are always safe, but calling a function that might not exist is a real Stof-specific gap — that's what the ? prefix checks, across an entire chain at once, however many calls are in it. (?. also exists, for a single hop, but in practice ? covering the whole chain is almost always what you want.)
sub: {
object: {}
fn at(query: str) -> unknown {
self.get(query)
}
}
fn subobj() -> obj {
self.sub
}
#[main]
fn main() {
// a library function that doesn't exist
pln(?Std.dne());
// an object/function that doesn't exist
pln(?self.dne.myfunc());
// missing partway through a longer chain
pln(?self.subobj().object.dne());
// doesn't interfere with a chain that succeeds
pln(?self.subobj()["object"]);
// pairs naturally with ?? for a default
pln(?self.subobj()["dne"].woops().dude() ?? "default");
}Ternary Operator
condition ? if_true : if_false — the usual shorthand for a two-branch if-expression:
#[main]
fn main() {
let value = null;
value = value ? value : "default";
pln(value);
}Block Expressions for Initialization
A { } block is itself an expression — the last statement without a trailing ; is its value. That makes it a clean way to collapse a multi-step initialization into a single assignment:
#[main]
fn main() {
let value = null;
value = {
if (value == null) "default"
else value
};
pln(value);
}Most editors let you fold a block like this away once it's written, so a complex initialization doesn't have to clutter the function around it.