Skip to content

Types & no-null ​

Tantu is statically typed, and it has no null. This page covers how types work and how Tantu models "no value" and "failure" as ordinary data.

The inference boundary ​

You annotate signatures; Tantu infers bodies. Function parameter types and non-Unit return types are required; local let/var types are usually inferred from the initializer.

tantu
fn twice(x: Int) -> Int:     # signature: annotated
    let y = x + x            # body: y is inferred as Int
    y

fn main() -> Unit:
    print(twice(21))
Try it

Add an annotation to a local when the type can't be inferred, most commonly an empty list:

tantu
fn main() -> Unit:
    let empty: List[Int] = []   # without the annotation this is E0107
    print(len(empty))
Try it

Immutability ​

let bindings can't be reassigned; var bindings can. Assigning to a let is a compile error (E0102).

tantu
fn main() -> Unit:
    let x = 1
    x = 2          # error E0102: cannot assign to immutable binding 'x'

Lists are immutable by convention: the builtins that "change" a list (push, concat) return a new one and leave the original alone.

No null: Option[T] ​

Absence is a value of type Option[T]: either Some(value) or None. There is no way to get a "null pointer": the type system forces you to handle the None case.

tantu
fn first_even(xs: List[Int]) -> Option[Int]:
    for x in xs:
        if x % 2 == 0:
            return Some(x)
    None

fn main() -> Unit:
    match first_even([1, 3, 4, 7]):
        Some(n) => print(n)
        None    => print("none found")
Try it

Recoverable failure: Result[T, E] ​

Result[T, E] is Ok(value) or Err(error). Use it when a caller might want to know why something failed.

tantu
fn parse_num(s: Str) -> Result[Int, Str]:
    match parse_int(s):
        Some(n) => Ok(n)
        None    => Err("not a number: " + s)

fn main() -> Unit:
    match parse_num("42"):
        Ok(n)  => print(n)
        Err(e) => print(e)
Try it

The ? operator ​

? unwraps an Option or Result, and on None/Err it early-returns from the enclosing function. It turns a chain of matches into a straight line.

tantu
fn parse_pair(s: Str) -> Result[(Int, Int), Str]:
    let parts = split(s, ",")
    if len(parts) != 2:
        return Err("expected two fields: " + s)
    let a = parse_num(parts[0])?      # on Err(e), parse_pair returns Err(e) here
    let b = parse_num(parts[1])?
    Ok((a, b))

fn parse_num(s: Str) -> Result[Int, Str]:
    match parse_int(s):
        Some(n) => Ok(n)
        None    => Err("not a number: " + s)

fn main() -> Unit:
    match parse_pair("3,4"):
        Ok((a, b)) => print(a + b)
        Err(e)     => print(e)
Try it

The rule: ? on a Result requires the enclosing function to return a Result with the same error type; ? on an Option requires it to return an Option. Mixing the two is a type error (E0106); convert explicitly with match.

Panics ​

The failure mode that isn't a value is a panic: division by zero, an out-of-range list index, a send on a closed channel, or deadlock. A panic prints a stack trace and terminates the whole program with exit code 1; there is no catch in v1. Prefer get (which returns Option) over indexing when an index might be out of range.