Alpha. Vary is under active development and not ready for production use. Syntax, APIs, performance, and behaviour may change between releases.

Generics

Generics let you write code that works with any type. Instead of writing separate functions for a list of integers and a list of strings, you write it once using a placeholder like T. The compiler fills in the actual type each time you use it, so you get type safety without repeating yourself.

Go shipped without generics for over a decade, and some developers preferred it that way. The tradeoff is real: deeply nested types like Dict[Str, List[Pair[Int, Int]]] are hard to read. But without generics you end up duplicating code or losing type safety by falling back to Any. Vary includes generics but keeps them simple: optional bounds via interface constraints, no variance annotations, and types are inferred at call sites so you rarely write them out.

Design choices

ChoiceDecisionRationale
SyntaxSquare brackets [T]Angle brackets <T> conflict with comparison operators in the parser. Square brackets are unambiguous and match Python's type hint syntax.
InferenceAlways inferred at call sitesYou never write type arguments explicitly. The compiler infers them from the values you pass. This keeps call sites clean.
ErasureType erasure on the JVMSame approach as Java. Type parameters are checked at compile time and erased in bytecode. No runtime overhead, no reified types.
BoundsOptional interface boundsType parameters can declare an upper bound: [T: Comparable]. The compiler enforces the constraint at every call site and allows calling bound interface methods on the type parameter. Unbounded parameters are still supported.
VarianceInvariant onlyNo covariance or contravariance annotations. List[Cat] is not a subtype of List[Animal]. This avoids the complexity of variance rules and the unsoundness problems that come with them.

Generic classes

Type parameters go in square brackets after the class name:

class Box[T](value: T) {
    let value: T = value

    def get(self) -> T {
        return self.value
    }
}

let int_box = Box(42)
let str_box = Box("hello")
print(int_box.get())        # 42
print(str_box.get())        # hello

Type arguments are inferred from constructor arguments. You write Box(42), not Box[Int](42).

Multiple type parameters are comma-separated:

class Pair[A, B](first: A, second: B) {
    let first: A = first
    let second: B = second
}

let p = Pair(1, "one")
print(p.first)              # 1
print(p.second)             # one

Generic data types

Data types work the same way:

data Entry[K, V] {
    key: K
    value: V
}

let e = Entry("name", 42)
print(e)                    # Entry(key=name, value=42)

Generic functions

Functions declare type parameters after the function name:

def identity[T](x: T) -> T {
    return x
}

def first[T](items: List[T]) -> T {
    return items[0]
}

def swap[A, B](pair: (A, B)) -> (B, A) {
    let (a, b) = pair
    return (b, a)
}

print(identity(42))         # 42
print(identity("hello"))    # hello
print(swap((1, "x")))       # (x, 1)

The type parameter is inferred from the argument, so you call identity(42) without specifying the type.

Bounded type parameters

A type parameter can declare an upper bound using : InterfaceName. The compiler ensures every type argument satisfies the bound, and lets you call bound interface methods on the type parameter:

interface Printable {
    def display(self) -> Str {
    }
}

def show_all[T: Printable](items: List[T]) -> None {
    for item in items {
        print(item.display())
    }
}

Without the bound, calling item.display() would be a type error because the compiler wouldn't know that T has a display method.

Bounds work on classes and data types too:

class SortedPair[T: Comparable](a: T, b: T) {
    let first: T = a
    let second: T = b
}

If the type argument doesn't satisfy the bound, the compiler reports an error:

Type 'Int' does not satisfy bound 'Printable' on parameter 'T'

Built-in generic types

The standard library uses generics throughout:

TypeDescription
List[T]Ordered, mutable collection
Dict[K, V]Key-value mapping
Set[T]Unordered unique elements
Result[T, E]Success or failure
Task[T]Async task handle
let nums: List[Int] = [1, 2, 3]
let ages: Dict[Str, Int] = {"Alice": 30}
let tags: Set[Str] = {"a", "b"}