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
| Choice | Decision | Rationale |
|---|---|---|
| Syntax | Square brackets [T] | Angle brackets <T> conflict with comparison operators in the parser. Square brackets are unambiguous and match Python's type hint syntax. |
| Inference | Always inferred at call sites | You never write type arguments explicitly. The compiler infers them from the values you pass. This keeps call sites clean. |
| Erasure | Type erasure on the JVM | Same approach as Java. Type parameters are checked at compile time and erased in bytecode. No runtime overhead, no reified types. |
| Bounds | Optional interface bounds | Type 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. |
| Variance | Invariant only | No 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:
| Type | Description |
|---|---|
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"}