Type System

Nostos features a powerful and flexible type system based on Algebraic Data Types (ADTs), structural typing, and type inference. It emphasizes safety and expressiveness without the need for verbose type annotations.

Algebraic Data Types (ADTs)

ADTs are fundamental for modeling complex data. Nostos supports two primary forms: Sum Types (Variants) and Product Types (Records).

Sum Types (Variants)

Variants allow a type to be one of several possibilities. They are perfect for modeling states or mutually exclusive data.


# Option and Result are built-in - no need to define them!
# Option[T] = Some(T) | None()
# Result[T, E] = Ok(T) | Err(E)

# A Shape can be a Circle, Rectangle, or Triangle
type Shape = Circle(Float) | Rectangle(Float, Float) | Triangle(Float, Float)

# Pattern matching functions at top level
area(Circle(radius)) = 3.14 * radius * radius
area(Rectangle(width, height)) = width * height
area(Triangle(base, height)) = 0.5 * base * height

main() = {
    my_circle = Circle(10.0)
    my_rect = Rectangle(5.0, 8.0)

    println(area(my_circle))  # 314.0
    println(area(my_rect))    # 40.0
}

Product Types (Records)

Records are analogous to structs or objects, grouping named fields together. They can be immutable by default or explicitly mutable with var type.


# Immutable Point record
type Point = {x: Float, y: Float}

# Mutable record (var type allows field mutation)
var type DataBuffer = {data: List[Int], size: Int}

main() = {
    p = Point(10.0, 20.0)   # Positional construction
    x_val = p.x             # Field access

    # For mutable records (var type)
    buf = DataBuffer([1, 2], 2)
    buf.size = 3            # Direct mutation is allowed

    println(x_val)
    println(buf.size)
}

Record Update (Functional Update)

Create a new record with some fields updated while copying the rest from an existing record. The original record is not modified.


type Point3D = {x: Int, y: Int, z: Int}

main() = {
    p = Point3D(1, 2, 3)

    # Syntax: Type(base, field: newValue, ...)
    p2 = Point3D(p, x: 10)        # {x: 10, y: 2, z: 3}
    p3 = Point3D(p, x: 10, z: 30) # {x: 10, y: 2, z: 30}

    println(p2.x)  # 10
    println(p3.z)  # 30
}

Newtypes

Newtypes wrap an existing type to provide type-safety and prevent accidental misuse. They are single-field variants.


type UserId = UserId(Int)
type Email = Email(String)

process_user(id: UserId, email: Email) = {
    println("Processing user " ++ show(id) ++ " with email " ++ show(email))
}

main() = {
    user_id = UserId(123)
    user_email = Email("test@example.com")

    process_user(user_id, user_email)

    # This would be a type error:
    # process_user(123, "test@example.com")

    # Unwrap the value (usually through pattern matching or a .value accessor)
    (UserId(id_val)) = user_id
    println("Raw user ID: " ++ show(id_val))
}

Type Aliases

Type aliases provide alternative names for existing types, improving readability without creating a new distinct type.


# Newtypes provide type-safe wrappers
type Milliseconds = Milliseconds(Int)
type Seconds = Seconds(Int)

# Functions can require specific wrapper types
waitMs(Milliseconds(ms)) = println("Waiting " ++ show(ms) ++ "ms")
waitSec(Seconds(s)) = println("Waiting " ++ show(s) ++ "s")

main() = {
    # Type-safe - can't accidentally mix up time units
    waitMs(Milliseconds(1000))
    waitSec(Seconds(5))

    # This would be a type error:
    # waitMs(Seconds(5))  # ERROR: expected Milliseconds, got Seconds
}

Generics

Nostos supports generics, allowing you to write functions and types that work with any type, or types constrained by traits, without sacrificing type safety.


# Option[T] and Result[T, E] are built-in generics - use them directly!

# Define your own generic types
type Pair[A, B] = Pair(A, B)
type MyList[T] = MyNil | MyCons(T, MyList[T])

# Generic functions
swap[A, B](Pair(a, b): Pair[A, B]) -> Pair[B, A] = Pair(b, a)
myFirst[T](MyCons(x, _): MyList[T]) -> T = x

main() = {
    # Use built-in Option
    int_opt: Option[Int] = Some(42)
    str_opt: Option[String] = Some("hello")

    # Use custom generics
    p = Pair(1, "one")
    swapped = swap(p)  # Pair("one", 1)

    println(int_opt)
    println(swapped)
}