LEVIATHAN v962456e · 962456eee1

Types

Unions and optionals

Closed unions T | U, the optional T?, narrowing with != None and is, and the ?? and ?. operators.

since 0.1.0-alpha.1linuxwindowswasm

Description

A union type int | string holds a value of exactly one of the listed types. A value of type T can be assigned to any union that contains T.

T? is shorthand for T | None. None is the type of the single value that means "absent". There is no null. An optional field starts as None, and a general union starts with the default value of its first member.

You cannot call a method or read a member through a union until the compiler knows which alternative you hold. Test the value, and inside the tested branch the type is narrowed:

Narrowing an optional and a union

string describe(int | string v) {
    if (v is int) {
        return "int ${v + 1}";
    } else {
        return "string ${v.toUpper()}";
    }
}

int lengthOf(string? s) {
    if (s != None && s.length() > 0) {
        return s.length();
    }
    return 0;
}

console.writeln(describe(41));
console.writeln(describe("hi"));
console.writeln(lengthOf("abc"));
console.writeln(lengthOf(None));
int 42
string HI
3
0

Rules

The types.

  • int | string is a closed, tagged union. Each stored value knows which alternative it holds.
  • T? means T | None. None has no payload and never compares equal to a present value, so an absent value, an empty value and a filled value stay distinguishable: with int? count = None, count == 0 is false.
  • A field of an optional type defaults to None. A union field or variable of another kind defaults to the default of its first member.
  • A numeric optional such as float? keeps None and converts a present value. See lang.numeric-conversions.

Narrowing.

  • Calling a member of a union value, or reading one, is a compile error until it is narrowed: "narrow the union before calling members".
  • x != None, x == None and x is T narrow x. The narrowing applies in the branches of if and else, in the body of a while loop, in the arms of a conditional expression c ? a : b, and across && (s != None && s.length() > 0). A path such as req.host narrows like a variable.
  • ! negates what the test established: after if (!(s == None)) the value is a present string.
  • Assigning to the narrowed variable or path, or to something it is reached through, drops the narrowing.
  • Narrowing does not carry past an early return. After if (s == None) { return 0; } the value is still string? on the next line. Write the use inside the enclosing if (s != None) { ... } block, or in the else branch.

Defaults and chaining.

  • a ?? b yields a when it is present and b otherwise. Its type is the union with None removed, and b must match it: int n = s ?? 5; is an error when s is a string?.
  • a?.m() and a?.b stop and give None when a is None; the arguments are not evaluated. The result has type R | None.

Examples

Defaults and optional chaining:

?? and ?. on optional values

class Req {
    string? host;
    int | string id = 5;
}

Req r = Req();
console.writeln(r.host == None);
console.writeln(r.id is int);
string? name = None;
console.writeln(name ?? "anonymous");
r.host = "example.org";
console.writeln(r.host?.toUpper());
name = "ada";
console.writeln(name?.length());
string? nothing = None;
console.writeln(nothing?.length() == None);
true
true
anonymous
EXAMPLE.ORG
3
true

Narrowing a path in a loop:

Narrowing in a while loop

class Node {
    int value = 0;
    Node? next;
}

Node tail = Node();
tail.value = 3;
Node head = Node();
head.value = 1;
head.next = tail;
Node? cur = head;
int total = 0;
while (cur != None) {
    total += cur.value;
    cur = cur.next;
}
console.writeln(total);
4

Notes

?? and ?. work on an optional without narrowing it first.

See also

  • Type expressions — How a type is written: names, qualified names, generic instantiations, unions, optionals and function types.