Types

Types are the foundation of data structures in Monad. This chapter covers inductive types, the primary way to define new types.

Basic Syntax

Types are declared with the type keyword:

type Colour {
    red,
    green,
    blue
}

This defines a type Colour with three constructors.

Constructors with Fields

Constructors can carry data:

type Shape {
    circle (radius : I64),
    rectangle (width : I64) (height : I64)
}

Natural Numbers

The canonical example of an inductive type is the natural numbers, defined in the prelude as:

type Nat {
    zero,
    succ (n : Nat)
}

This defines:

  • zero: the natural number 0
  • succ n: the successor of n (i.e. n + 1)

So 3 is represented as succ (succ (succ zero)).

Lists

Lists are defined inductively:

type List A {
    empty,
    cons (a : A) (List A) : List A
}

The type parameter A makes this polymorphic:

  • List I64: a list of 64-bit integers
  • List String: a list of strings
  • List (List Bool): a list of boolean lists

List Literals

Monad supports list literal syntax [a, b, c], which desugars through the FromListLiteral class:

def nums : List I64 := [1, 2, 3]
// Desugars to:
// FromListLiteral.cons 1 (FromListLiteral.cons 2
//   (FromListLiteral.cons 3 FromListLiteral.empty))

The annotation matters: the literal is polymorphic in its container, so the checker needs the target type to pick the instance.

Tuples

Parenthesised comma-separated values are tuple literals. They desugar to right-nested Pair.pair applications, so (x, y, z) is Pair.pair x (Pair.pair y z):

def point : Pair I64 I64 := (3, 4)
def triple : Pair I64 (Pair String Bool) := (1, "two", true)

(x,) and (x) both mean just x.

Result Type

For representing computations that may fail:

type Result E A {
    ok (a : A),
    err (e : E)
}

Pattern Matching on Types

When defining functions over a type, use pattern matching:

def is_zero (n : Nat) : Bool :=
  match n {
    zero => true,
    succ _ => false
  }

def pred (n : Nat) : Nat :=
  match n {
    zero => Nat.zero,
    succ m => m
  }

def plus (n : Nat) (m : Nat) : Nat :=
  match n {
    zero => m,
    succ k => Nat.succ (plus k m)
  }

Constructors used in a pattern are written bare (zero, succ k), but constructors used to build a value need their qualified name (Nat.succ) unless the type has been opened. The prelude opens Bool, Option, Result, and Unit, which is why true, some, and ok work bare everywhere.

Recursive Functions

Functions over inductive types can be recursive:

def is_empty {A : Type} (self : List A) : Bool :=
  match self {
    empty => true,
    cons a tail => false
  }

def append {A : Type} (a : List A) (b : List A) : List A :=
  match a {
    empty => b,
    cons el_a tail => List.cons el_a (append tail b)
  }

def first {A : Type} (self : List A) : Option A :=
  match self {
    empty => none,
    cons a tail => some a
  }

Each of these recurses on a structural subterm of its argument, so the termination checker accepts them without an attribute. (List.is_empty, List.append, and List.first already exist in the prelude — these are shown as illustrations.)

Strict Positivity

A recursive type must mention itself in a strictly positive position. Its own name may appear to the right of an arrow, or as an argument to something else, but it may not sit to the left of an arrow — a constructor field whose type is a function from the type being declared:

type Bad {
    mkBad (f : Bad -> I64)      // rejected
}
error: non-strictly positive occurrence of Bad in Bad
  --> bad.mo

Both compilers apply the rule and both name the type; they differ only in how they frame the error. The host adds an In Bad: header line and a source span (error: … at 1:1, then --> bad.mo:1:1), where this compiler's syntax tree carries no span to print, so it puts the type's name after in instead. The wording of the message itself is the same on both sides.

The rule is about polarity, and it flips once per arrow. Recursing in the codomain is fine (type Fwd { mkFwd (k : I64 -> Fwd) }), and so is an arrow whose domain is itself an arrow, because two flips land back on positive (type Neg { mkNeg (h : (Neg -> I64) -> I64) }). What is rejected is an odd number of flips between the declaration and the occurrence — which is exactly the shape that lets you write a non-terminating term without any recursion at all.

This is checked for type, not for struct. The two compilers agree on the rule and both reject the example above.

Type Parameters

Types can have type parameters for polymorphism:

type Option A {
    some (a : A),
    none
}

type Result E A {
    ok (a : A),
    err (e : E)
}

Built-in Types

Monad provides these types in the prelude, available without any import:

TypeConstructorsDescription
UnitunitSingle value
Booltrue, falseBoolean
I64 … I8(primitive)Signed integers, 64/32/16/8 bit
U64 … U8(primitive)Unsigned integers, 64/32/16/8 bit
F64, F32(primitive)Floats
Stringof_bytesUTF-8 string
Charof_bytesOne Unicode codepoint, written 'x' — but see the caveat below
Natzero, succNatural numbers
List Aempty, consLinked list
Option Asome, noneOptional value
Result E Aok, errSuccess or error
Pair A BpairTwo-element product; the target of tuple syntax
Void(none)Empty type
AnyanyExistential wrapper
IO AioThe IO monad
TruetrivialTrivially true proposition (in Prop)
Eq A a breflPropositional equality (in Prop)
Vec n Anil, consLength-indexed vector

True, Eq, and Vec are the dependently typed corner of the prelude — see Dependent Types.

Char is a stub type. Character literals ('M', '\n', 'λ') parse, type-check and compile in both implementations, but Char has no operations at all — no BEq, no ToString, no Char.* functions. A Char can be written, typed, passed and stored; nothing can inspect one. Use String for text you need to work with.

Summary

  • type defines new types through constructors
  • Pattern matching destructures values, one constructor level at a time
  • Recursive functions operate on inductive types
  • Type parameters (A) make types polymorphic
  • A recursive occurrence must be strictly positive
  • Tuple syntax (a, b) is sugar for Pair

Next, we'll explore type classes, Monad's mechanism for ad-hoc polymorphism.