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)
}