Type System
On this page 25
Home features a powerful, expressive type system that combines the best aspects of modern programming languages. It provides strong static typing with excellent type inference, ensuring both safety and ergonomics.
Overview
The Home type system is designed around several key principles:
- Sound by default: Types guarantee runtime behavior
- Expressive: Rich type constructs for modeling complex domains
- Inferrable: Minimal annotations required in most cases
- Zero-cost abstractions: Type information is erased at compile time
Primitive Types
Home provides a comprehensive set of primitive types optimized for systems programming:
Integer Types
// Signed integers
let a: i8 = -128
let b: i16 = -32768
let c: i32 = -2147483648
let d: i64 = -9223372036854775808
let e: i128 = -170141183460469231731687303715884105728
let f: isize = -1 // Platform-dependent size
// Unsigned integers
let g: u8 = 255
let h: u16 = 65535
let i: u32 = 4294967295
let j: u64 = 18446744073709551615
let k: u128 = 340282366920938463463374607431768211455
let l: usize = 1 // Platform-dependent size
Floating-Point Types
let x: f32 = 3.14159
let y: f64 = 2.718281828459045
// Special values
let inf = f64.inf
let neg_inf = f64.neg_inf
let nan = f64.nan
Boolean and Character Types
let flag: bool = true
let letter: char = 'A'
let emoji: char = '\u{1F600}' // Unicode scalar value
Compound Types
Tuples
Tuples group multiple values of different types:
let point: (i32, i32) = (10, 20)
let mixed: (string, i32, bool) = ("hello", 42, true)
// Accessing tuple elements
let x = point.0 // 10
let y = point.1 // 20
// Destructuring
let (name, age, active) = mixed
Arrays
Fixed-size collections of homogeneous elements:
let numbers: [i32; 5] = [1, 2, 3, 4, 5]
let zeros: [i32; 100] = [0; 100] // Initialize with repeated value
// Array access
let first = numbers[0]
let length = numbers.len() // 5
// Compile-time bounds checking when index is known
let valid = numbers[4] // OK
// let invalid = numbers[5] // Compile error: index out of bounds
Slices
Dynamically-sized views into contiguous sequences:
let array = [1, 2, 3, 4, 5]
let slice: []i32 = array[1..4] // [2, 3, 4]
// Slice operations
let first = slice[0]
let len = slice.len() // 3
// Full slice
let full: []i32 = array[..]
Type Inference
Home's type inference engine eliminates most explicit type annotations:
// Types are inferred from context
let x = 42 // i32 (default integer type)
let y = 3.14 // f64 (default float type)
let z = "hello" // string
let list = [1, 2, 3] // [i32; 3]
// Inference through function calls
fn double(n: i32) -> i32 {
n _ 2
}
let result = double(21) // result: i32
// Inference in closures
let add = |a, b| a + b
let sum = add(1, 2) // Inferred as (i32, i32) -> i32
Type Aliases
Create meaningful names for complex types:
type UserId = u64
type Point2D = (f64, f64)
type Matrix4x4 = [[f64; 4]; 4]
type Result<T> = Result<T, Error>
type Callback<T> = fn(T) -> void
// Usage
let id: UserId = 12345
let origin: Point2D = (0.0, 0.0)
Optional Types
Home uses explicit optional types instead of null:
// Optional type syntax
let maybe_number: ?i32 = 42
let nothing: ?i32 = null
// Working with optionals
if let Some(n) = maybe_number {
print("Got: {n}")
}
// Optional chaining
let result = maybe_number?.to_string()
// Default values
let value = maybe_number ?? 0
// Unwrapping (panics if null)
let definitely = maybe_number.!
Union Types
Express values that can be one of several types:
type StringOrNumber = string | i32
fn process(value: StringOrNumber) {
match value {
string => print("String: {value}"),
i32 => print("Number: {value}"),
}
}
process("hello") // String: hello
process(42) // Number: 42
Intersection Types
Combine multiple type constraints:
trait Printable {
fn print(self)
}
trait Serializable {
fn serialize(self) -> []u8
}
// Type must implement both traits
fn save(item: Printable & Serializable) {
item.print()
let bytes = item.serialize()
// ...
}
Never Type
The never type represents computations that never complete:
// Functions that never return
fn infinite_loop() -> never {
loop {}
}
fn panic(message: string) -> never {
print("PANIC: {message}")
std.process.exit(1)
}
// Useful in match expressions
fn unwrap_or_panic<T>(opt: ?T) -> T {
match opt {
Some(value) => value,
None => panic("unwrap failed"), // never coerces to T
}
}
Phantom Types
Types used only at compile time for additional type safety:
struct Meters<phantom T> {
value: f64,
}
struct Validated {}
struct Unvalidated {}
fn validate(input: Meters<Unvalidated>) -> ?Meters<Validated> {
if input.value >= 0.0 {
Some(Meters { value: input.value })
} else {
null
}
}
// Type system prevents mixing validated/unvalidated data
let raw: Meters<Unvalidated> = Meters { value: 100.0 }
let validated = validate(raw).! // Meters<Validated>
Type Constraints
Constrain generic types with trait bounds:
// Single constraint
fn print_value<T: Display>(value: T) {
print("{value}")
}
// Multiple constraints
fn process<T: Clone + Debug + Send>(value: T) {
let copy = value.clone()
debug_print(copy)
}
// Where clauses for complex constraints
fn complex<T, U>(a: T, b: U) -> T
where
T: From<U> + Default,
U: Into<T>,
{
if a == T.default() {
b.into()
} else {
a
}
}
Associated Types
Types defined within traits:
trait Iterator {
type Item
fn next(mut self) -> ?Self.Item
}
struct Counter {
current: i32,
max: i32,
}
impl Iterator for Counter {
type Item = i32
fn next(mut self) -> ?i32 {
if self.current < self.max {
let value = self.current
self.current += 1
Some(value)
} else {
null
}
}
}
Existential Types
Hide concrete types behind abstract interfaces:
// Return type is hidden
fn make_iterator() -> impl Iterator<Item = i32> {
Counter { current: 0, max: 10 }
}
// The caller only knows it implements Iterator
let iter = make_iterator()
for item in iter {
print("{item}")
}
Const Generics
Use compile-time values as type parameters:
struct Array<T, const N: usize> {
data: [T; N],
}
impl<T, const N: usize> Array<T, N> {
fn new(default: T) -> Self where T: Copy {
Array { data: [default; N] }
}
fn len(self) -> usize {
N
}
}
let arr: Array<i32, 10> = Array.new(0)
assert(arr.len() == 10)
Edge Cases
Recursive Types
Recursive types require indirection:
// Direct recursion is not allowed
// struct Node { next: Node } // Error: infinite size
// Use Box for indirection
struct Node {
value: i32,
next: ?Box<Node>,
}
// Or use explicit reference
struct TreeNode {
value: i32,
left: ?_TreeNode,
right: ?*TreeNode,
}
Zero-Sized Types
Types with no runtime representation:
struct Unit {}
// ZSTs take no memory
let units: [Unit; 1000000] = [Unit {}; 1000000]
assert(std.mem.size_of::<[Unit; 1000000]>() == 0)
Covariance and Contravariance
Understanding variance in generic types:
// Covariant in T (can substitute subtypes)
struct Box<T> {
value: T,
}
// Invariant (no substitution allowed)
struct Cell<T> {
value: mut T,
}
// Contravariant in argument position
type Consumer<T> = fn(T) -> void
Best Practices
-
Prefer type inference: Let the compiler infer types when clear
// Good let numbers = [1, 2, 3, 4, 5] // Unnecessary let numbers: [i32; 5] = [1, 2, 3, 4, 5] -
Use type aliases for clarity: Name complex types
type HttpHeaders = HashMap<string, []string> type ResponseHandler = fn(Response) -> Result<void, Error> -
Prefer optionals over sentinel values:
// Good fn find(items: []Item, id: u64) -> ?Item // Avoid fn find(items: []Item, id: u64) -> Item // Returns "empty" Item if not found -
Use newtypes for type safety:
struct UserId(u64) struct OrderId(u64) // Prevents accidentally mixing IDs fn get_user(id: UserId) -> User fn get_order(id: OrderId) -> Order -
Constrain generics appropriately: Only require what you need
// Too restrictive fn count<T: Clone + Debug + Eq + Hash>(items: []T) -> usize // Just right fn count<T>(items: []T) -> usize { items.len() }