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