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 overListitself, as inf : 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 anya). 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
Pattern can be destructured to access 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.