Advanced Types

Zolo provides a rich type system with generics, type aliases, newtypes, associated types, and as/is operators for casting and type checking.

Generics #

Generic Functions #

Use angle brackets to declare type parameters:

fn first<T>(items: [T]) -> T? {
    if items.len() == 0 { return nil }
    return items[0]
}

fn identity<T>(x: T) -> T {
    return x
}

print(first([1, 2, 3]))      // Some(1)
print(first([] as [str]))    // nil
print(identity("hello"))     // "hello"
print(identity(42))          // 42

Generic Structs #

struct Pair<A, B> {
    first: A,
    second: B,
}

impl Pair<A, B> {
    fn new(first: A, second: B) -> Pair<A, B> {
        return Pair { first, second }
    }

    fn swap(self) -> Pair<B, A> {
        return Pair { first: self.second, second: self.first }
    }
}

let p = Pair.new(1, "one")
print("{p.first}: {p.second}")

let swapped = p.swap()
print("{swapped.first}: {swapped.second}")

Generic Enums #

enum Tree<T> {
    Leaf(T),
    Node(Tree<T>, Tree<T>),
}

fn sum_tree(tree: Tree<int>) -> int {
    match tree {
        Tree.Leaf(v)       => v,
        Tree.Node(l, r)    => sum_tree(l) + sum_tree(r),
    }
}

let t = Tree.Node(
    Tree.Node(Tree.Leaf(1), Tree.Leaf(2)),
    Tree.Leaf(3)
)
print(sum_tree(t))  // 6

Where Clauses #

Use where to constrain generic type parameters to specific traits:

fn print_all<T>(items: [T]) where T: Displayable {
    for item in items {
        print(item.display())
    }
}

fn largest<T>(items: [T]) -> T where T: Comparable {
    let mut max = items[0]
    for item in items {
        if item > max { max = item }
    }
    return max
}

Multiple constraints can be combined:

fn debug_sort<T>(items: [T]) -> [T] where T: Comparable + Displayable {
    let sorted = Array.sort(items)
    for item in sorted {
        print(item.display())
    }
    return sorted
}

Type Aliases #

type creates an alias for an existing type. Aliases are interchangeable with their underlying type:

type UserId = int
type Username = str
type Scores = {str: int}

fn get_user(id: UserId) -> Username {
    // ...
}

let scores: Scores = { "Alice": 95, "Bob": 87 }

Generic Type Aliases #

type Result2<T> = Result<T, str>
type Pair<A, B> = (A, B)
type Matrix<T> = [[T]]

fn safe_divide(a: int, b: int) -> Result2<int> {
    if b == 0 { return Result.Err("division by zero") }
    return Result.Ok(a / b)
}

Newtypes #

newtype wraps a type to create a distinct type that is not interchangeable with its inner type. This prevents accidentally mixing semantically different values:

newtype Meters(float)
newtype Seconds(float)
newtype Kilograms(float)

fn speed(distance: Meters, time: Seconds) -> float {
    return distance.0 / time.0
}

let d = Meters(100.0)
let t = Seconds(9.58)
print("Speed: {speed(d, t)} m/s")

// This would be a type error:
// speed(t, d)  -- wrong order, caught at compile time

Newtype with Methods #

newtype Email(str)

impl Email {
    fn new(s: str) -> Result<Email, str> {
        if s.contains("@") {
            return Result.Ok(Email(s))
        }
        return Result.Err("invalid email: {s}")
    }

    fn domain(self) -> str {
        return self.0.split("@")[1]
    }
}

let email = Email.new("user@example.com").unwrap()
print(email.domain())  // "example.com"

Newtype with Decorators #

@builder
newtype Config(str)

Type Casting with as

Use as to cast between numeric types:

let x: int = 42
let f: float = x as float
let b: int = 3.99 as int   // truncates to 3

let big: int = 1_000_000
let small: float = big as float

Type Checking with is

Use is to check the runtime type of a value:

fn describe(val: any) -> str {
    if val is int    { return "integer: {val}" }
    if val is float  { return "float: {val}" }
    if val is str    { return "string: \"{val}\"" }
    if val is bool   { return "boolean: {val}" }
    return "unknown type"
}

print(describe(42))        // "integer: 42"
print(describe(3.14))      // "float: 3.14"
print(describe("hello"))   // string: "hello"
print(describe(true))      // "boolean: true"

is with Pattern Matching

fn process(val: any) {
    match val {
        v if v is int   => print("Got int: {v}"),
        v if v is str   => print("Got str: {v}"),
        _               => print("Unknown"),
    }
}

Associated Types in Traits #

Traits can define associated types — types that implementing structs must specify:

trait Container {
    type Item

    fn add(self, item: Self.Item)
    fn get(self, index: int) -> Self.Item?
    fn len(self) -> int
}

struct Stack<T> {
    items: [T],
}

impl Container for Stack<T> {
    type Item = T

    fn add(self, item: T) {
        self.items = [...self.items, item]
    }

    fn get(self, index: int) -> T? {
        if index < 0 or index >= self.items.len() { return nil }
        return self.items[index]
    }

    fn len(self) -> int {
        return self.items.len()
    }
}

Multiple Return Values (Tuples) #

Functions can return multiple values using tuples:

fn min_max(nums: [int]) -> (int, int) {
    let mut lo = nums[0]
    let mut hi = nums[0]
    for n in nums {
        if n < lo { lo = n }
        if n > hi { hi = n }
    }
    return (lo, hi)
}

let (min, max) = min_max([3, 1, 4, 1, 5, 9])
print("Min: {min}, Max: {max}")

// Ignore one value with _
let (_, top) = min_max([7, 2, 9, 4])
print("Max: {top}")

Type Inference #

Zolo infers types in most contexts — you rarely need explicit annotations:

let x = 42          // inferred as int
let s = "hello"     // inferred as str
let items = [1,2,3] // inferred as [int]

// Explicit when needed
let empty: [str] = []
let nullable: int? = nil
enespt-br