Null & Initialization
Null values, initialization, & practical usage.
The null value is a controversial one in computer science, sometimes referenced as a mistake.
Putting personal opinion aside, the null concept is prevalent in a lot of languages and data formats. Because Stof's purpose is to hold all of the stuff and glue environments together, it has a null value concept.
The main problem with null is that it subverts a language's type system. For example, a str
can be a string OR null, which can be a pain.
Fortunately, Stof has a few features that help when working with null values.
Not Null Types
First on the list is the never null type! Any type with a "!" postfix operator will error when assigning a null value to it, ensuring it is never null.
#[main]
fn main() {
let value: int! = 42;
value = null; // errors
}
Null Check
It's very common in languages that have a null value to check for null via if statements.
#[main]
fn main() {
let value = null;
if (value == null) {
// doing null things
}
if (!value) {
// also null things, but any falsey value in general
}
}
Stof does have a shortcut for this, via the "??" operator. Given a left-hand side and right-hand side expression lhs ?? rhs
, it expands into "return left-hand side if it is not null, otherwise, return the right-hand side".
#[main]
fn main() {
let value = null ?? 42;
assert_eq(value, 42);
// chain them as you'd like
const field_val = self.field.another.value ?? self.field.other ?? "default";
assert_eq(field_val, "default");
}
Optional Chain
Even with field paths always being null-checked, it's common to run into a scenario where you're not sure whether a function exists before attempting to call it (rather unique to Stof).
In many languages, the "?." chaining operator exists to check whether the value is null before attempting to use it, but this does not cover the Stof pitfall of functions not existing in the document when called.
One way to handle this is the try-catch block.
Another way is the "?" prefix operator, which null-checks an entire chain of operations at once (no matter how many function calls are in the chain).
sub: {
object: {
// nothing here
}
fn at(query: str) -> unknown { self.get(query) }
}
fn subobj() -> obj { self.sub }
#[main]
fn main() {
// library function that does not exist
let res = ?Std.dne(); // null
// objects and/or functions that do not exist
res = ?self.dne.myfunc(); // null
// entire chain at once
res = ?self.subobj().object.dne(); // null
// doesn't interfere with successful calls
res = ?self.subobj()["object"]; // self.sub.object
// null-checks the entire chain of operations
res = ?self.subobj()["dne"].woops().dude(); // null
// partial ? - everything invalid before ? would error
res = self.subobj()["object"]?.woops(); // null
}
Ternary Operator
Another shortcut for a classic if-expression is the ternary operator: condition ? expr_if_true : expr_if_false
. This can also be used to check that values exist, although typically applied elsewhere.
#[main]
fn main() {
let value = null;
value = value ? value : "default";
assert_eq(value, "default");
}
Block Expressions
Stof block expressions wind up in a lot of places (especially async block expressions). However, another use case is to make sure values are initialized properly.
If a value has a complex initialization sequence or a lot of dependency checks, this might be easiest. In most code editors, you can collapse away the initialization logic.
#[main]
fn main() {
let value = null;
value = {
if (value == null) "default" // remember, no ";" means a return statement
else value
};
assert_eq(value, "default");
}
Last updated
Was this helpful?