Options

When a value is allowed to be either some type or null, it’s an option. Mark a type as optional by suffixing it with ?.

let a: int = 0;
a = null; // compile error: `a` is int, never null

let b: int? = 0;
b = null; // valid

An option is mimas’s answer to the billion-dollar mistake: null only exists where a ? invites it, so the compiler can guarantee you never trip over an unexpected null access.

An option is not its inner type

int? and int are different types. You can’t use a possibly-null value where a definitely-present one is required — you have to deal with the null case first (unwrapping, below).

let a: int = 0;
let b: int? = null;
a = b; // compile error: `b` may be null

Comparing options

You can compare an option for equality against null, or against a value of its inner type:

let maybe: int? = null;

print(maybe == null); // true
print(maybe == 0);    // false

Ordering comparisons (<, >, …), on the other hand, need both sides to be non-null — there’s no sensible place for null on a number line. Unwrap or coalesce first.

let a: int = 0;
let b: int? = null;
print(a > b); // compile error: these values can't be compared like numerals

Creating options

Any type coerces into its option form when it meets a null. So an array literal with a null in it, or an if whose branches disagree about presence, infers an optional type automatically.

let a = [0, null, 1];               // a is [int?]
let b = ~{ x = null, y = 0 };       // b is ~{int?}
let c = if ready { 0 } else { null }; // c is int?

The coercion only kicks in when a fresh value is created. It won’t quietly punch a null into an existing non-optional slot:

let d = [0];
d[0] = null; // compile error: expected int, found null

Flattening

There is no “option of an option.” If a type would come out as T??, mimas automatically flattens it to T? — the same choice Kotlin makes.

fn maybe() -> int? { null }

// `maybe()` is already int?, and the other branch is null, so this would be int??
// — mimas flattens it straight to int?.
let nested: int? = if ready { maybe() } else { null };

Option chaining

To reach through a value that might be null, you’d otherwise write a guard:

let len: int? = if name == null {
    null
} else {
    name.len()
};

The ?. operator collapses that into one expression. If the receiver is null, the whole chain short-circuits to null and the call is never made; otherwise it proceeds and re-wraps the result as an option.

let len: int? = name?.len();

The same works for indexing, with ?[ ]:

let grid: [[int]?] = [[0], null, [1]];
let cell: int? = grid[1]?[0]; // null — the middle row is null, so we stop

Unwrapping

When a code path expects an option to actually hold a value, you unwrap it. Where ? poses the question of an option, a postfix ! asserts the answer: “this is not null.” If it turns out to be null, that’s a runtime error.

let port: int = lookup_port()!; // `port` is a plain int from here on

When you’d rather supply a fallback than risk a runtime error, reach for ??, which yields its right side when the left is null:

let port: int = lookup_port() ?? 8080; // the value if present, otherwise 8080

And to branch on presence while binding the unwrapped value, use if let:

if let port = lookup_port() {
    connect(port);
} else {
    use_default();
}

When you want the unwrapped value for the rest of the scope and would rather bail out than nest, a let/else keeps things flat:

let port? = lookup_port() else {
    panic("no port configured");
};
// `port` is a plain int from here on