Advanced Types

Oxide's type system is powerful and flexible. Let's explore some advanced type features that enable you to write expressive, type-safe code.

Type Aliases

Type aliases give an existing type another name:

type Kilometers = Int
type Pounds = Int

fn main() {
    let distance: Kilometers = 5
    let weight: Pounds = 50

    // These are the same underlying type, so they can be mixed
    let total = distance + weight

    println!("Total: \(total)")  // Prints: Total: 55
}

The key point: type aliases create aliases, not new types. Kilometers and Pounds are both Int, so values of these types can be used interchangeably.

When to Use Type Aliases

Use aliases to:

  1. Reduce repetition - Shorten long type names
  2. Clarify intent - Make code more readable
  3. Refactor - Change a type in one place
// Reduce repetition
type Result<T> = std.result.Result<T, String>

fn readFile(path: &str): Result<String> {
    // ...
}

fn parseJson(json: &str): Result<Value> {
    // ...
}

The Result type alias shows up throughout the standard library, making error handling code more readable.

Generics in Type Aliases

Type aliases can be generic:

type Callback<T> = (T) -> T

fn applyTwice<T>(f: Callback<T>, x: T): T {
    f(f(x))
}

fn main() {
    let double: Callback<Int> = { x -> x * 2 }
    println!("\(applyTwice(double, 5))")  // Prints: 20
}

The Never Type !

The ! type, called the "never type," represents a function that never returns:

fn fail(msg: &str)! {
    panic!("\(msg)")
}

fn loopForever()! {
    loop {
        println!("Forever!")
    }
}

fn main() {
    // Never type is compatible with any type
    let x: Int = if condition {
        5
    } else {
        fail("Error!")  // Returns !
    }
}

Why Never Type Matters

The never type is useful in several situations:

// In match expressions
fn example(x: Int) {
    let msg = match x {
        1 -> "one",
        2 -> "two",
        _ -> panic!("Unknown"),  // Returns !
    }
}

// In Option handling
fn getOrPanic(opt: Int?): Int {
    opt.unwrapOrElse { panic!("No value") }
}

// In loops
fn keepAsking(): String {
    loop {
        let input = getUserInput()
        if valid(input) {
            return input
        }
        println!("Invalid!")
    }
}

The never type allows these patterns to work because the compiler understands that ! can be treated as any type.

Dynamically Sized Types (DSTs)

Most types have a size known at compile time. But some types, called dynamically sized types (DSTs), don't:

// Sized type - compiler knows the size
let x: Int = 5

// DST - compiler doesn't know the size
let x: Vec<Int> = [1, 2, 3]  // Error: Vec<Int> requires explicit construction

You can't use DSTs directly because Oxide needs to know the size at compile time. Instead, use references or pointers:

let x: &[Int] = &[1, 2, 3]  // OK: reference to a slice
let x: &str = "hello"       // OK: reference to a str

// Trait objects are also DSTs
let obj: &dyn Clone = &value  // OK: reference to trait object

Deref Coercion

Oxide automatically converts between types when using the Deref trait. This is called deref coercion:

fn takesStr(s: &str) {
    println!("\(s)")
}

fn main() {
    let s = "hello".toString()
    takesStr(&s)  // Coerces String to &str
}

Under the hood, Oxide is calling the deref method:

  • String implements Deref<Target = str>
  • So &String is coerced to &str

The Deref Trait

You can implement Deref for your own types:

import std.ops.Deref

struct MyBox<T> {
    value: T,
}

extension<T> MyBox<T>: Deref {
    type Target = T

    fn deref(): &T {
        &value
    }
}

fn main() {
    let x = MyBox { value: 5 }
    println!("\(*x)")  // Prints: 5
}

Function Pointers

Function pointers let you pass functions like values:

fn add(a: Int, b: Int): Int {
    a + b
}

fn multiply(a: Int, b: Int): Int {
    a * b
}

fn executeOperation(op: (Int, Int) -> Int, a: Int, b: Int): Int {
    op(a, b)
}

fn main() {
    let result1 = executeOperation(add, 5, 3)
    println!("add: \(result1)")  // Prints: add: 8

    let result2 = executeOperation(multiply, 5, 3)
    println!("multiply: \(result2)")  // Prints: multiply: 15
}

Function Pointers vs Closures

Function pointers (fn) are different from closure types:

// Function pointer - implements Fn, FnMut, FnOnce
let f: (Int) -> Int = { x -> x * 2 }

// Closure - captures environment
let multiplier = 3
let g = { x -> x * multiplier }  // Can't assign to fn type!

// But closures can be assigned to function pointer types if they don't capture
let h: (Int) -> Int = { x -> x * 2 }  // OK: no captured variables

When to use each:

  • Function pointers (fn) - When you need a simple function type without closure capture
  • Closures - When you need to capture variables from the environment
  • Trait objects (&dyn Fn) - For maximum flexibility

Function Item Types

When you write a function name without calling it, you get its item type:

fn add(a: Int, b: Int): Int {
    a + b
}

fn main() {
    // These are all equivalent
    let f = add             // Function item type (Int, Int) -> Int
    let g: (Int, Int) -> Int = add
    let h = add as (Int, Int) -> Int

    println!("\(f(5, 3))")  // Prints: 8
}

Function Traits

All functions and closures implement one of the Fn* traits:

// Regular function
fn regular(x: Int): Int { x * 2 }

// Closure with no captures
let noCapture = { x: Int -> x * 2 }

// Closure with immutable capture
let value = 3
let immutCapture = { x -> x * value }

// Closure with mutable capture
var counter = 0
let mutCapture = { counter += 1 }

// Closure that takes ownership
let owned = "hello".toString()
let moveCapture = move { owned.len() }

All of these can be used as function parameters:

fn applyTwice<F>(f: F, x: Int): Int
where
    F: Fn(Int) -> Int,
{
    f(f(x))
}

fn main() {
    println!("\(applyTwice(regular, 2))")        // Prints: 8
    println!("\(applyTwice(noCapture, 2))")      // Prints: 8
    println!("\(applyTwice(immutCapture, 2))")   // Prints: 27
}

Generic Trait Bounds

You can use complex trait bounds to express sophisticated type constraints:

// Multiple bounds with +
fn process<T>(item: T)
where
    T: Clone + Display + Debug,
{
    // Can use Clone, Display, and Debug methods
}

// Higher-ranked bounds
fn takesRefs<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> UInt,
{
    // f can accept &str with any lifetime
}

// Where clauses for clarity
fn example<T, U>(t: T, u: U)
where
    T: Clone,
    U: Clone,
    T: Display,
    U: Display,
{
    // Clearer than T: Clone + Display, U: Clone + Display
}

Type Inference Limitations

Oxide's type inference is powerful but not unlimited:

fn main() {
    // Inference works
    let v = vec![1, 2, 3]  // Inferred as Vec<Int>

    // Sometimes you need to help
    let v: Vec<Int> = vec![]  // Can't infer Int from empty vec

    // Turbofish syntax for explicit types
    let v = Vec<Int>.new()
    let nums = "1,2,3".split(",").map { s -> s.parse<Int>().unwrap() }.collect<Vec<Int>>()
}

Phantom Types

Sometimes you want a generic parameter that doesn't actually store a value:

import std.marker.PhantomData

struct PhantomType<T> {
    data: Int,
    phantom: PhantomData<T>,  // Has size 0
}

extension<T> PhantomType<T> {
    static fn new(data: Int): Self {
        PhantomType {
            data,
            phantom: PhantomData,
        }
    }
}

fn main() {
    let p1: PhantomType<String> = PhantomType.new(5)
    let p2: PhantomType<Int> = PhantomType.new(5)

    // These are different types even though they have the same data
}

Phantom types are useful for:

  • Maintaining type information without storing it
  • Implementing type-safe abstractions
  • Working with unsafe code

Generic Specialization

Sometimes you want different implementations for different types:

// Generic implementation
extension<T> Vec<T>: Clone
where
    T: Clone,
{
    fn clone(): Self {
        // Clone each element
    }
}

// Specialized for Copy types (faster)
extension Vec<Int>: Clone {
    fn clone(): Self {
        // Can use memcpy because Int is Copy
    }
}

Advanced Example: Type-Safe Builder

Here's a real-world pattern using advanced types:

struct Builder<S> {
    name: String?,
    age: Int?,
    phantom: PhantomData<S>,
}

trait BuilderState {}
struct NoName;
struct HasName;
struct Complete;

extension NoName: BuilderState {}
extension HasName: BuilderState {}
extension Complete: BuilderState {}

extension Builder<NoName> {
    static fn new(): Self {
        Builder {
            name: null,
            age: null,
            phantom: PhantomData,
        }
    }

    consuming fn name(name: String): Builder<HasName> {
        Builder {
            name: name,
            age: self.age,
            phantom: PhantomData,
        }
    }
}

extension Builder<HasName> {
    consuming fn age(age: Int): Builder<Complete> {
        Builder {
            name: self.name,
            age: age,
            phantom: PhantomData,
        }
    }
}

extension Builder<Complete> {
    consuming fn build(): Person {
        Person {
            name: name.unwrap(),
            age: age.unwrap(),
        }
    }
}

struct Person {
    name: String,
    age: Int,
}

fn main() {
    // Compile error: can't build without setting both fields
    // let p = Builder.new().build()

    // OK: set both fields
    let p = Builder.new()
        .name("Alice".toString())
        .age(30)
        .build()

    println!("Person: \(p.name), age \(p.age)")
}

This pattern uses phantom types to enforce at compile time that the builder is in the correct state.

Summary

Advanced types in Oxide:

  • Type aliases - Give names to complex types for clarity
  • Never type - Represents functions that don't return
  • DSTs and Deref - Work with unsized types safely
  • Function pointers - Pass functions as values
  • Function traits - Flexible function parameters
  • Generic bounds - Express sophisticated constraints
  • Phantom types - Type information without storage
  • Specialization - Different implementations for different types

These features combine to give Oxide a type system that's both expressive and safe, letting you write code that's both correct by construction and readable.