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.
| 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. |
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
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)
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.
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'
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"}