8.2 Primitive, Composite, Function, and User-Defined Types
Once a compiler can talk about “type” at all, it needs a vocabulary rich enough to describe both simple values and structured ones. A language usually begins with primitive types such as integers, booleans, characters, and floating-point numbers. It then adds type constructors that build larger shapes: arrays, tuples, records, functions, options, references, classes, and more.
The key idea is that a composite type is not just a bag of primitive pieces. The way those pieces are arranged changes the semantics.
| kind | example | what matters |
|---|---|---|
| primitive | int, bool | atomic value category |
| array | int[] | repeated element type, often length/mutability rules |
| tuple | (int, bool) | position and arity |
| record | { value: int, ok: bool } | field names and field types |
| function | (int) -> bool | parameter types and return type |
| user-defined named type | UserId | declared identity, not just representation |
Type Constructors Build New Meaning
A language may contain int and bool, but the following types do very different jobs:
int[]
(int, bool)
{ value: int, ok: bool }
(int) -> boolAll four mention int and bool, but they are not interchangeable.
int[]means “many integers of the same category.”
(int, bool)means “two values packed by position.”
{ value: int, ok: bool }means “named fields with separate roles.”
(int) -> boolmeans “a callable thing that consumes one type and produces another.”
That is why type checking must understand both the atoms and the constructors. Merely scanning for which primitive names appear is not enough.
Function Types Are Contracts, Not Just Syntax
A function type is one of the most important composite types because it connects multiple parts of a program. At minimum, it records parameter types and a return type:
(int, int) -> intThat signature says much more than “this thing has parentheses.” It tells the checker how many arguments are expected, which conversions are legal, and what type subsequent expressions may assume after the call.
Stronger type systems may add even more information: thrown exceptions, effects, ownership, mutability, nullability, or lifetimes. But the central pattern stays the same. A function type is a promise from caller to callee and back again.
User-Defined Types Protect Meaning
If a compiler treats every identity-like value as plain int, then UserId, OrderId, and Meters can be mixed accidentally as soon as they share a representation. That is often unacceptable. A user-defined nominal type lets the language say, “these values may look similar underneath, but they carry different meaning.”
This is where design policy enters:
| design choice | strength | risk |
|---|---|---|
| treat everything structurally | easy reuse | semantically different values may mix |
| use nominal types for important domains | stronger protection | more explicit conversions may be needed |
Structural Shape Versus Declared Identity
Some values are naturally shape-oriented. A Point { x: int, y: int } may be useful across many libraries as long as the fields line up. Other values are identity-oriented. A UserId should not silently become a ProductId just because both are stored as integers.
Good type-system design usually distinguishes these cases deliberately instead of forcing one rule everywhere.
Worked Comparison
Suppose two modules both define:
{ x: int, y: int }If the language is structural, those values may interoperate immediately. If the language is nominal, two separate declarations may produce two separate types even though the field layout matches. Neither policy is automatically better. Structural compatibility favors reuse. Nominal distinction favors semantic protection.