Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Generics

Generics let you write a record, enum, or machine once and reuse it across many element types. Type parameters appear in square brackets after the name. Lake resolves them by monomorphisation: every concrete use site is compiled into its own specialized copy, so there is no runtime cost.

Generic Records

Add [T] (or several parameters) to a record declaration. Fields may then use the type parameter:

Box[T] is { val T }

Pair[K V] is { k K  v V }

Construction infers the type parameters from the argument types:

main is {
  _ -> {
    let b = Box(42)            -- Box[i64]
    let p = Pair(1 "hello")    -- Pair[i64 buf]
  }
}

Generic Machines

A machine can be generic too. Its parameters are bound from the call arguments and may appear in the parameter types and the ret type:

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

main is {
  _ -> {
    let b = Box(42)
    let v = unbox(b)   -- T = i64, v : i64
  }
}

A trivial identity machine shows the shape clearly:

id[T] is {
  x T -> ret T { ret x }
}

Generic Enums

Enums accept type parameters in exactly the same way. Option[T] and Result[T E] are the canonical examples (covered in the Enums chapter):

Option[T] is enum {
  Some(T)
  None
}

Result[T E] is enum {
  Ok(T)
  Err(E)
}

unwrap_or[T] is {
  o Option[T] d T -> ret T {
    when o {
      Some(v) -> { ret v }
      None    -> { ret d }
    }
  }
}

Use-Site Types and Annotations

Write a concrete instantiation by filling the brackets: Vec[i64], Box[i64], Result[i64 buf]. You often need this on let bindings whose right-hand side does not, on its own, pin down the type parameters — for example a constructor that returns an empty collection, or a payload-free enum variant:

main is {
  _ -> {
    let v Vec[i64] = vec_new()        -- vec_new() has no element to infer from
    let y Option[i64] = Option.None    -- None carries no payload
    let r Result[i64 buf] = Result.Ok(7)
  }
}

When the arguments already determine the type — like Box(42) or Option.Some(42) — the annotation is optional.

Two Generics, Same Name

Because dispatch is by argument type, two generic machines that share a name can coexist as long as they live in different modules and take distinguishable argument types. The compiler picks the right one from the call’s argument types. This is how unwrap_or works for both Option[T] (in std.option) and Result[T E] (in std.result).

Notes and Limitations

  • Monomorphisation happens at compile time; each distinct instantiation becomes its own specialized function. There is no boxing or dynamic dispatch.
  • Value/return inference sometimes needs an explicit annotation on an intermediate binding. If the compiler can’t infer a type parameter, annotate the let (as above).
  • IntMap[V] is keyed by i64. A fully generic hash map (arbitrary key types) is not available yet — see the Standard Library chapter.