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