Lake

Generics

Generics let a record, enum or machine work over any type, fixed at each use site. Lake uses monomorphisation: for every concrete type a generic is used with, the compiler emits a specialised copy — there is no boxing and no runtime type information, so generic code is exactly as fast as hand-written code.

§Generic syntax: square brackets

Type parameters are written in [ ]:

lake
Box[T] is {
  value T
}

unbox[T] is {
  b Box[T] -> ret T {
    ret b.value
  }
}

Box[i64] and Box[buf] become two distinct concrete records at compile time; unbox is specialised for each.

§Using generic types

lake
let b Box[i64] = Box(42)
let n = unbox(b)            # 42

The annotation Box[i64] fixes T. Where the compiler can infer the type parameter from the arguments it does so automatically; where it cannot (for example a zero-argument constructor), annotate the binding.

§The collections are generic

The standard library's containers are generic over their element type:

lake
+std.vec.{ Vec vec_new vec_push vec_get }

main is {
  _ -> {
    let v0 Vec[i64] = vec_new()
    let v1 = vec_push(v0 10)
    let v2 = vec_push(v1 20)
    let x = vec_get(v2 1)        # 20
  }
}

Vec[i64], Vec[Point], IntMap[buf] are all separate monomorphised types. See the standard library for the full container set.

§Bounds: constraining a type parameter

A type parameter can be required to satisfy a protocol:

lake
same[T: Eq] is {
  a T b T -> ret i64 {
    ret eq(a b)
  }
}

[T: Eq] means same accepts any T that implements Eq.” The compiler verifies the bound at each instantiation: if you call same with a type that has no eq, it is a compile error, not a runtime one.

§Key ideas

  • Type parameters use square brackets: Vec[T], unbox[T], same[T: Eq].
  • Monomorphisation specialises per concrete type — zero-cost, no boxing.
  • Inference fixes T from arguments; annotate where it can't (e.g. let v Vec[i64] = vec_new()).
  • Bounds ([T: Eq]) constrain a parameter to a protocol, checked at compile time.