Lesson 6.2

Monk's Type Model

Nine kinds, one optional flag, and one compatibility function. The whole type system in three parts.

concepts go 12 min read

A type is just a description of what a value can be.

The checker needs to reason about values without running the program. To do that, it works with types -- compact descriptions that say "this variable holds an integer" or "this function takes two floats and returns a bool."

Monk's type representation is intentionally small. The entire type model lives in a single Go struct with two fields. Everything else -- arrays, records, functions, optionals -- is expressed through those two fields.

Nine kinds cover every Monk value.

The first field is Kind -- an enum with nine values, one for each category of Monk value:

type Kind int

const (
    KindAny    Kind = iota  // unknown / flows-through-anything
    KindInt                  // 64-bit integer
    KindFloat                // 64-bit float
    KindStr                  // string
    KindBool                 // boolean
    KindNone                 // the none value
    KindArray                // array (with element type)
    KindRecord               // record (with field types)
    KindFunc                 // function (with param and return types)
)

KindAny is the escape hatch. When the checker can't determine a type statically -- for example, the return type of a builtin that works on both arrays and strings -- it uses KindAny. Any value is assignable to Any, and Any is assignable to anything. It's a deliberate release valve, not an oversight.

Why not more kinds? Monk's design goal is a type system that non-experts can understand and use. Nine kinds is enough to catch real bugs without requiring type annotations everywhere. More expressiveness would mean more annotation burden.

Optional is orthogonal to kind.

Every type in Monk can be made optional by appending ?. An optional type accepts either the base type or none. This is represented as a single boolean flag, separate from the kind:

type Type struct {
    Kind     Kind
    Optional bool
    // ... element/field/param types for compound kinds
}

This means int? is represented as Kind: KindInt, Optional: true. You don't need a separate KindOptionalInt variant for each base type. The orthogonal flag means you get T? for any T -- including string[]? or (int) -> bool?.

In Monk source code, you write the ? suffix after the type name:

let name string? = none        // ok -- none is valid for string?
let age  int?   = 42           // ok -- int is valid for int?

let count int   = none         // error: none is not assignable to int

AssignableTo: one function encodes all compatibility rules.

The checker's core question is always the same: "can this value go here?" A string can't go in an int slot. A none can go in an int? slot but not an int slot. An int can go in a float slot because Monk allows numeric widening.

All of these rules live in one function: AssignableTo(src, dst Type) bool. The rules it encodes:

1

Identity. intint. Same kind, same optional flag -- always assignable.

2

Any wildcard. Any → anything, anything → Any. The escape hatch for builtins and unresolvable types.

3

Numeric widening. intfloat. An integer is always valid where a float is expected. The reverse is not true -- no implicit narrowing.

4

Optional acceptance. noneT?, and TT?. The optional destination accepts both its base type and none.

5

Structural records. A record literal is assignable to a record type if it has exactly the right fields with the right types. Structural, not nominal -- the type name doesn't matter, the shape does.

6

Element-wise arrays. int[]int[] only. The element type must match (or be assignable to) the destination element type.

7

Exact function signatures. (int) -> bool(int) -> bool. Param count, param types, and return type must all match exactly.

Centralizing these rules in one function means they apply consistently everywhere -- variable declarations, assignments, function arguments, array literals, record fields. The checker calls AssignableTo in dozens of places, but the rules live in one.

How types appear in source code.

Monk uses space-separated type annotations, not colon syntax. This was a deliberate parser design choice -- colons would be ambiguous with record literals.

// Variables
let x int = 42
let name string = "Alice"
let scores float[] = [1.0, 2.5, 3.7]
let user User = { name: "Bob", age: 30 }

// Optional variables
let maybe int? = none

// Functions -- types on params, return after arrow
let add = (a int, b int) int {
    return a + b
}

// Function-typed parameters
let apply = (f (int) -> int, n int) int {
    return f(n)
}

// Custom type definitions
type Point { x float, y float }
type ID = int    // type alias

When there's no annotation, the checker infers the type from the first assignment. The inferred type is then enforced on every subsequent assignment -- you get the benefits of inference without losing the safety of a declared type.

Forward references and hoisting.

Monk programs often use functions before they're defined. A recursive function calls itself. Two functions call each other. This is normal -- and the checker handles it.

Before running the main checking pass, the checker does a first scan over the AST and records all function declarations and their type signatures. This pre-population of the scope means functions can call each other in any order without the checker complaining about an "undeclared" identifier.

// Works fine -- isEven calls isOdd which is defined below
let isEven = (n int) bool {
    if n == 0 { return true }
    return isOdd(n - 1)
}

let isOdd = (n int) bool {
    if n == 0 { return false }
    return isEven(n - 1)
}

Key takeaways

1

Nine kinds (Any, Int, Float, Str, Bool, None, Array, Record, Func) cover every Monk value.

2

Optional is an orthogonal boolean flag. int? = KindInt + Optional:true. Works for any T.

3

AssignableTo encodes all compatibility in one function: identity, Any, numeric widening, optional, structural records, array elements, exact function signatures.

4

Type inference fixes a variable's type on first assignment. No annotation required, but the type is then enforced.