Type System
Hindley-Milner Inference
Section titled “Hindley-Milner Inference”Nulang uses Algorithm W (Hindley-Milner) for global type inference. You rarely need to write type annotations — the compiler infers them:
// The compiler infers: fn compose[A,B,C](f: B -> C, g: A -> B) -> A -> Cfn compose(f, g) { fn(x) { f(g(x)) }}Explicit annotations are supported and encouraged for public APIs:
fn compose[A, B, C](f: B -> C, g: A -> B) -> A -> C { fn(x: A) -> C { f(g(x)) }}Primitive Types
Section titled “Primitive Types”| Type | Description | Example |
|---|---|---|
Int |
48-bit signed integer (i64-tagged value representation) | 42 |
Float |
64-bit IEEE 754 float | 3.14 |
Bool |
Boolean | true, false |
String |
UTF-8 string | "hello" |
Unit |
Unit value (like void) |
() |
Nil |
Nil/null | nil |
Never |
Uninhabited type (bottom) | — |
Row-Polymorphic Records
Section titled “Row-Polymorphic Records”Records are structurally typed with row polymorphism. When a function parameter has no type annotation, the record row stays open and accepts any record with the needed fields:
// Inferred parameter — accepts ANY record with 'x' and 'y' fieldsfn distance(point) { to_float(point.x * point.x + point.y * point.y)}
// Works with extra fieldslet p3d = { x: 1, y: 2, z: 3 }perform IO.print(perform Int.to_string(distance(p3d))) // OK — extra fields are fineAn explicit annotation creates a closed record that requires an exact field count:
fn origin(): { x: Int, y: Int } = { x: 0, y: 0 }Closed record annotations reject records with extra or missing fields. Use inferred parameters when you want row polymorphism.
Reference Capabilities
Section titled “Reference Capabilities”Inspired by Pony, Nulang uses reference capabilities for data-race freedom:
| Capability | Deny Read | Deny Write | Sendable | Description |
|---|---|---|---|---|
iso |
Yes | Yes | Yes | Isolated, unique reference |
trn |
No | Yes | No | Transitional, write-unique |
ref |
No | No | No | Mutable, shared-nothing |
val |
No | Yes | Yes | Immutable, shareable |
box |
Yes | No | No | Read-only |
tag |
Yes | Yes | Yes | Opaque, identity-only |
lineariso |
Yes | Yes | Yes | Linear isolated (at-most-once) |
Capabilities are compile-time only and erased at runtime. There are no runtime capability checks.
// val reference: immutable and shareable (can be sent between actors)let shared = "hello" :cap valperform IO.print(shared)Algebraic Data Types
Section titled “Algebraic Data Types”Sum types via type declarations:
type Option[T] = Some(T) | Nonetype Result[T, E] = Ok(T) | Err(E)type List[T] = Cons(T, List[T]) | NilGeneric type parameters use [T, U, ...] syntax and are inferred at call sites.
Effect Types
Section titled “Effect Types”Every function carries an effect row. Pure functions have an empty effect row. Effectful functions declare their effects:
// Pure: no effectsfn add(x: Int, y: Int) -> Int = x + y
// Effectful: performs IOfn greet() -> Unit ! {IO} { perform IO.print("Hello")}See Algebraic Effects for the full effect system.