Nil Safety
When a value may be nil, Zolo provides three complementary operators that
eliminate explicit checks and verbose chains of if.
The ?. operator navigates fields or calls methods safely: if the receiver is
nil, the entire expression short-circuits to nil instead of throwing an
error.
?. on struct fields and on the result of functions that may return nil.
// Feature: Optional chaining `?.`
// Syntax: `obj?.field` — if `obj` is `nil`, propagates `nil`;
// otherwise, accesses the field.
// When to use: navigate a chain of fields where any link could be
// `nil`, without nested if-else. Combined with `??` it gives a
// safe default.
struct Address {
city: str,
zip: str,
}
struct User {
name: str,
address: Address,
}
let alice = User { name: "Alice", address: Address { city: "Portland", zip: "97201" } }
// Plain access — no `?.` when you know it is not nil.
print(alice.name) // Alice
print(alice.address.city) // Portland
// Optional chain — safe even if there is a nil in the middle.
let city = alice?.address?.city ?? "Unknown"
print(city) // Portland
// When user is nil, `?.` short-circuits.
let missing: User? = nil
let city2 = missing?.address?.city ?? "Unknown"
print(city2) // Unknown
// Useful in lookups that may fail.
fn find(id: int) -> User? {
if id == 1 { return alice }
return nil
}
let zip1 = find(1)?.address?.zip ?? "00000"
let zip2 = find(99)?.address?.zip ?? "00000"
print(zip1) // 97201
print(zip2) // 00000
// Chain on a method also works.
let upper = find(1)?.name.to_upper() ?? "?"
print(upper) // ALICE
The ?? operator provides a default value when the left-hand side is nil.
Unlike ||, it only fires for nil — falsy values like 0, false, or an
empty string pass through without triggering the fallback. Chaining ?. and
?? in the same expression is common.
Simple fallback, chaining ??, and the distinction between nil and falsy values.
// Feature: Null-coalescing operator `??`
// Syntax: `value ?? fallback` — if `value` is `nil`, returns
// `fallback`; otherwise, returns `value`.
// When to use: define a default when something can be `nil`.
// Reads as: "or else". Unlike `||`, it does NOT treat `0`, `false`
// or empty string as falsy — only `nil` triggers the fallback.
// Simple default.
let name: str? = nil
print(name ?? "Anonymous") // Anonymous
let real_name: str? = "Alice"
print(real_name ?? "Anonymous") // Alice
// Chained defaults.
let primary: str? = nil
let secondary: str? = nil
let tertiary = "Default"
let chosen = primary ?? secondary ?? tertiary
print(chosen) // Default
// Combined with `?.` — the classic safe-navigation case.
struct Config {
theme: str,
}
let cfg: Config? = nil
let theme = cfg?.theme ?? "dark"
print(theme) // dark
// `??` does NOT treat 0/false as nil — only nil itself.
let zero: int? = 0
print(zero ?? 42) // 0 (not 42!)
let empty: str? = ""
print(empty ?? "fallback") // (empty string)
// Result of a function that may fail.
fn lookup(key: str) -> int? {
if key == "a" { return 1 }
if key == "b" { return 2 }
return nil
}
print(lookup("a") ?? -1) // 1
print(lookup("z") ?? -1) // -1
The postfix ? operator is "nil propagation": inside a function whose return
type is T?, writing expr? makes the function return nil immediately
if expr is nil. This is the equivalent of Rust's ? operator applied to
optionals.
? chains fallible functions without an if let at every step; any nil aborts
and propagates to the caller.
// Feature: Try operator `?` (early-return on optional)
// Syntax: `expr?` — if `expr` is `nil`, returns `nil` from the
// current function; otherwise, "unwraps" the value.
// When to use: chain calls that may fail (`-> T?`) without a nested
// if-let on every step. The CALLING function must also return `T?` —
// `?` propagates the nil upward.
// Function that may fail.
fn parse_int(s: str) -> int? {
if s == "x" { return nil }
if s == "" { return nil }
return 42 // mock — pretends it parsed
}
// `?` propagates nil. If `parse_int` returns nil, `process` also
// returns nil without needing `if let`/`match`.
fn process(s: str) -> int? {
let v = parse_int(s)? // if nil, returns nil from here
return v + 1
}
print(process("ok") ?? -1) // 43
print(process("x") ?? -1) // -1 (parse failed, ? propagated nil)
print(process("") ?? -1) // -1
// Chain of stages — any failure aborts.
fn lookup_name(id: int) -> str? {
if id == 1 { return "Alice" }
return nil
}
fn first_letter(s: str) -> str? {
if s.len() == 0 { return nil }
return s.sub(1, 1)
}
fn initial(id: int) -> str? {
let name = lookup_name(id)? // may fail
let letter = first_letter(name)? // may fail
return letter.to_upper()
}
print(initial(1) ?? "?") // A
print(initial(99) ?? "?") // ? (lookup failed)
// `?` also works inside expressions.
fn double_if_found(id: int) -> str? {
return lookup_name(id)?.to_upper()
}
print(double_if_found(1) ?? "?") // ALICE
print(double_if_found(99) ?? "?") // ?
Challenge
Write a function greet(id: int) -> str? that uses lookup_name(id)? from the
example and returns "Hello, {name}!" when the id exists, or nil when it
does not. Test with greet(1) ?? "not found" and greet(99) ?? "not found".