Skip to main content

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:

not-null.stof
#[main]
fn main() {
  let value: int! = 42;

  try { value = null; }
  catch { pln('blocked: value can never be null'); }

  pln(value);
}
Output

Null Checks

The usual tools work as expected — == null, and any falsy check via !value:

null-check.stof
#[main]
fn main() {
  let value = null;
  pln(value == null, !value);

  const fallback = value ?? 42;
  pln(fallback);
}
Output

?? 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:

path-safety.stof
#[main]
fn main() {
  const field_val = self.field.another.value ?? self.field.other ?? "default";
  pln(field_val);
}
Output

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.)

optional-chain.stof
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");
}
Output

Ternary Operator

condition ? if_true : if_false — the usual shorthand for a two-branch if-expression:

ternary.stof
#[main]
fn main() {
  let value = null;
  value = value ? value : "default";
  pln(value);
}
Output

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:

block-init.stof
#[main]
fn main() {
  let value = null;
  value = {
      if (value == null) "default"
      else value
  };
  pln(value);
}
Output

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.