Skip to content

10. The type system

The chapters so far query and transform data with the types HQL provides. This one is about the types a program declares for itself, and the mechanisms that relate one type to another: records and unions, results, subtypes, generics, receiver calls, operators and traits. The Mighty Types corpus has one program per topic.

One relation symbol

Everything the type system says is said with :. Between a value and a type it states what the value is, pages : Int. Between two types under type it states that one narrows the other, type Card : Doc. Between a type parameter and a type or trait it is a bound, T : Orderable. The three readings are one relation: a value of the narrower type is usable wherever the wider one is expected, and a bound asks for exactly that.

type Named { name : String }
type Person : Named { born : Int }

fn greet(n: Named) : String = fmt("hello, {}", n.name)

greet(Person(name = "Ada", born = 1815))
"hello, Ada"

A Person is a Named, so greet takes one with no step and no check at run time.

Three mechanisms

Three mechanisms end with a value used at a stronger type than it started with, and the chapter keeps them apart.

Mechanism Written The question it answers
Subtyping type Card : Doc Which types stand in for which?
Generics type Stack<T> What does this declaration leave unknown?
Refinement d \| is(Point) What is known about this one value?

Subtyping relates whole types. Generics write a declaration once for a family of types, and a bound is where the two meet: T : Doc restricts an unknown by the subtype relation. Refinement is a statement about one value, established by a test that can fail, and the value itself is never changed. Chapter 11 is refinement at the boundary where data arrives.

Capabilities are traits

What a type is and what can be done with it are two different statements. type Card : Doc says a card is a document. That + adds two values of a type, or that a generic function may call area on its parameter, is a capability, stated by implementing a trait for the type. A trait is not a type: nothing is held as a Shape, and a trait's name appears in a bound, an implementation and another trait's declaration, nowhere else.

type Vec2 { x : Int, y : Int }

impl Add for Vec2 {
    fn add(self, other: Vec2) : Vec2 = Vec2(x = self.x + other.x, y = self.y + other.y)
}

Vec2(x = 1, y = 2) + Vec2(x = 3, y = 4)
Vec2(x = 4, y = 6)

Types in modules

A module declares types, traits and implementations as it declares functions, and a public type is visible by its name to every unit that imports the module. Chapter 5 writes and imports modules, and visibility keeps a type or trait private to its module. An implementation belongs with its trait or with its type: a program implements its own trait for any type and any trait for its own type, and never a core trait for a built-in one.

The pages

Page What it covers
Records fields, the constructor, optional and nested fields, generic records, constructors a program declares
Unions and match a type that is one of several, match covering every case, guards, literal and type patterns
Results Result<T, E>, ok and err, the ways out, and why there is no unwrap
Subtypes and narrowing type A : B, supertype sets, abstract types, widening by annotation, narrowing by test, conversion by construction
Generics type parameters, bounds, generic traits and the inference that fills them
Receiver calls value.f(args), how the name resolves, and the three spellings of one call
Operators the core traits behind +, ==, <, xs[i] and xs[a:b]
Traits required functions and default bodies, bounds, a trait extending a trait, and how a trait differs from inheritance