Types

Roc is statically typed, and types are inferred—you rarely have to write them, but you can, and any annotation you write is checked.

Roc's Type System

Roc uses Hindley–Milner type inference with a few deliberate restrictions:

  • Rank-1. Quantification happens once per definition, at the outermost level. A definition can be polymorphic (identity : a -> a), but a function argument is used at a single type—i.e. you can't take an (a -> a) argument and apply it at several element types within one call (that needs rank-2).
  • No higher-kinded polymorphism. Type variables range over types (Str, List(U64)), never over type constructors—you can't abstract over List itself, as in f : m(a) -> m(b).
  • No subtyping. Types relate by unification, not a sub/supertype lattice. The width flexibility of records and tag unions is expressed with extension variables (see Structural Types), not subtyping.

Generalization

A definition is generalized—made reusable at many types—only in these cases:

  • Functions are always generalized; each call site is checked at its own types.
  • Number literals default rather than generalize: an unsuffixed literal resolves to a concrete type, ultimately falling back to Dec. See numbers.
  • An explicitly annotated value is generalized to its annotated scheme. Annotating a value with a free type variable opts into a type scheme, so the binding is generalized to it (empty : List(a) is then reusable at any a). Note that we report an error for top-level values with free vars, so in practice this only applies to let-defs.
  • A value alias—a binding whose right-hand side is a bare reference to an already-generalized binding (shorthand = Foo.my_func)—stays generalized, since copying a reference does no work and so is safe to reuse at many types.

Every other value is monomorphic: one type, fixed by its definition and uses. This is what stops a value (or its dbg/expect) from being silently recomputed at each type it might otherwise take.

A mutable variable (var) is never generalized, even with an annotation: it has a single type, fixed by its first use. This is the value restriction in its original, soundness role—a polymorphic mutable cell could be written at one type and read back at another, so a var is always monomorphic.

Type Annotations

Annotate a definition by writing name : Type above it. Lowercase names in a type are type variables; repeating a name means the same type.

greeting : Str
greeting = "hello"

identity : a -> a
identity = |x| x

Capitalized declarations introduce types rather than values—see Nominal Types (:=) and Type Aliases (:).

Where Clauses

A where clause lists the methods a type variable must provide. Each constraint has the form var.method : signature:

join : List(a) -> Str where [a.to_str : a -> Str]

A where clause can appear on any annotation, including a value's:

items : List(a) where [a.to_str : a -> Str]
items = []

Structural Types

Structural types are defined by their shape: two of them are the same type when their shapes match, with no declaration required.

  • Records{ name : Str, age : U64 }. See records.
  • Tag unions[Ok(a), Err(e)]. See tag unions.
  • Tuples(Str, U64). See tuples.

Records and tag unions are either closed (exactly the listed fields or tags) or open, ending in an extension variable that stands for "and possibly more":

{ name : Str, .. }     # any record with at least a `name : Str` field
{ name : Str, ..r }    # the same, naming the rest `r`
[Red, Green, ..]       # this union, or any wider one
[Red, Green, ..u]      # the same, naming the rest `u`

An anonymous extension (..) is a fresh variable each time; a named one (..r) lets you refer to the same "rest" in more than one place.

Nominal Types

A nominal type is a distinct type with its own identity, declared with :=:

UserId := U64

UserId and U64 share a representation but are different types—unification will not silently mix them. Nominal types may take parameters (Tree(a) := …) and define associated methods in a trailing .{ } block.

Constructing Nominal Types

You construct a nominal value by writing its backing value where the nominal type is expected; the annotation (or surrounding context) supplies the identity.

Color := [Red, Green, Blue]
Point := { x : F64, y : F64 }
UserId := U64

c : Color
c = Red               # or `Color.Red`; tags with payloads use `Color.Tag(payload)`

p : Point
p = { x: 1, y: 2 }    # a bare record literal becomes a Point here

uid : UserId
uid = UserId.(0)      # a number literal needs explicit construction (see below)

You can also construct a nominal value explicitly by naming the type. This is required when no expected type drives the conversion—for example, returning a nominal from a function whose argument is the plain backing value:

Distance := U64
Pair := (U64, Str)

d = Distance.(26)         # value backing
pair = Pair.(1, "two")    # tuple backing
p = Point.{ x: 1, y: 2 }  # record backing
c = Color.Red             # tag backing (with a payload: `Color.Tag(payload)`)

to_distance : U64 -> Distance
to_distance = |n| Distance.(n)   # `|n| n` is a type error: a plain U64 is not a Distance

Structural literals—records and tags—coerce into a nominal type implicitly when the expected type supplies the identity (p and c above): the literal is the backing shape, so it lifts by unification.

Number and string literals do not implicitly become a nominal. They coerce only into a builtin number/string type, or into a nominal that declares its own from_numeral / from_quote. For any other nominal—including a transparent newtype like UserId := U64—use explicit construction (UserId.(0)).

A value that already has a concrete type—like the U64 parameter n above— must also be constructed explicitly; it does not silently become a different nominal.

Destructuring Nominal Types

Nominal types can be destructured with patterns to access their inner values.

Color := [Red, Green, Blue].{
    my_color : Color
    my_color = Red
    
    is_red : Color -> Bool
    is_red = |color| match color {
        Red => True
        _ => False
    }
}

expect my_color.is_red() # PASS

For records:

get_x : Point -> F64
get_x = |{ x }| x

Opaque Nominal Types

Declaring with :: instead of := makes a nominal type opaque: outside its defining module the backing representation is hidden, so the type can only be created and inspected through the methods that module exposes.

Token :: Str         # other modules see `Token`, never the `Str` inside

Inside the defining module an opaque type is constructed and destructured just like any nominal type; the restriction applies only to other modules.

Nested Nominal Types

Nominal types can be defined inside other nominal types using the associated items block:

Geometry := [].{
    Point := { x: F64, y: F64 }.{
        origin : Point
        origin = { x: 0, y: 0 }
    }

    Rectangle := { top_left: Point, bottom_right: Point }.{
        area : Rectangle -> F64
        area = |{ top_left, bottom_right }|
            width = bottom_right.x - top_left.x
            height = bottom_right.y - top_left.y
            width * height
    }
}

Nested types are accessed using dot notation:

rect = Geometry.Rectangle.{ top_left: Geometry.Point.origin, bottom_right: { x: 10, y: 10 } }

This is useful for grouping related types under a common namespace.

Type Aliases

A type alias names an existing type with : (not :=). Aliases are transparent— substituted away during compilation—so an alias and its definition are the same type and interchange freely:

Bytes : List(U8)
Pair : (U64, U64)

Reach for an alias when you only want a shorter or clearer name; reach for a nominal type when you want a genuinely distinct type the compiler keeps separate.

Mutually Recursive Types

TODO