This AND That, This OR That
In logic, we combine propositions with AND and OR. "It is raining AND I have an umbrella." "I will take the bus OR I will walk."
Types work the same way. We can combine simple types into complex ones using two fundamental operations: AND (having multiple things together) and OR (being one of several possibilities).
AND: Having Multiple Things
A chess piece has a color AND a kind. Not one or the other---both, together.
Piece:
color: Color // AND
kind: PieceKind // both presentThis is a product type (also called a record, struct, or object). It bundles multiple values into one.
Every piece has both fields. You can ask any piece for its color. You can ask any piece for its kind. The AND is a guarantee.
More examples:
Point:
x: Number AND
y: Number
Person:
name: Text AND
age: Number AND
email: Text
Move:
piece: Piece AND
from: Square AND
to: SquareThe AND type says: "I contain all of these, together."
OR: Being One of Several
A square on the chess board is empty OR contains a piece. Not both---one or the other.
Think of it like a box. You open a box and find either: nothing inside, or something inside. The box itself is always there---the question is what it contains.
SquareContent:
empty OR
piece: PieceThis is a sum type (also called a union, variant, or enum). It represents alternatives---mutually exclusive possibilities.
A value of this type is one of the alternatives, never multiple, never none. When you have a SquareContent, you must ask: which one is it?
if square is empty:
... handle empty square ...
else if square is piece:
... handle the piece ...More examples:
Color:
white OR black
PieceKind:
king OR queen OR rook OR bishop OR knight OR pawn
GameResult:
white_wins OR black_wins OR drawThe OR type says: "I am one of these, but which one varies."
Combining AND and OR
Real types combine both operations. Our chess state uses AND at the top level:
ChessState:
board: Board AND // has both
turn: Color But Color itself is an OR:
Color:
white OR black // is one ofAnd each square contains an OR:
SquareContent:
empty OR Piece Where Piece is an AND:
Piece:
color: Color AND
kind: PieceKindThe structure nests arbitrarily. AND within OR within AND. Each level adds precision to our description.
The Power of OR: Representing Possibilities
OR types are surprisingly powerful. They let us represent situations that would otherwise require awkward workarounds.
Absence: Maybe / Optional
Sometimes a value might not exist. A function that searches for something might find it---or might not.
find_piece_at(board, square):
... might return a Piece, or nothing ...We can model this with an OR type:
Maybe Piece:
nothing OR
just: PieceNow the type forces us to consider both cases:
result = find_piece_at(board, e4)
if result is nothing:
// square was empty
else if result is just piece:
// found a piece, use itThe OR type makes absence explicit. You can't accidentally ignore it.
Failure: Result / Either
Similarly, an operation might succeed or fail:
validate_move(state, move):
... might succeed, or fail with a reason ...We can model this:
Result:
success: ChessState OR
failure: ErrorNow every caller must handle both possibilities:
outcome = validate_move(state, attempted_move)
if outcome is success new_state:
// move was valid, continue with new_state
else if outcome is failure error:
// move was invalid, report the errorPattern Matching: Unpacking OR Types
When we have an OR type, we need to discover which alternative we have. This is called pattern matching---we match the value against each possible pattern.
match square_content:
case empty:
"no piece here"
case piece(color, kind):
"found a " + color + " " + kindThe beauty of pattern matching is that it's exhaustive---the compiler can check that we handled every possibility.
But here's something deeper: programmers don't just care about solving a problem once. We build things that evolve. Requirements change. New features arrive. Old code must adapt.
If we add a new alternative to the OR type, the compiler tells us everywhere we need to update:
// If GameResult adds a new case:
GameResult:
white_wins OR black_wins OR draw OR abandoned
// The compiler warns us:
match result:
case white_wins: ...
case black_wins: ...
case draw: ...
// Warning: 'abandoned' not handled!This is foundational. Types give us tools to understand the effects of change---to see what will break before it breaks. Without types, we rely on memory, discipline, testing after the fact. With types, the machine catches our oversights.
The Algebra of Types
There's a beautiful symmetry here. Types behave like numbers:
If Bool has 2 values (true, false) and Color has 2 values (white, black):
- AND is like multiplication:
- OR is like addition:
// AND: 2 × 2 = 4 possible values
BoolAndColor:
b: Bool AND c: Color
// (true, white), (true, black),
// (false, white), (false, black)
// OR: 2 + 2 = 4 possible values
BoolOrColor:
Bool OR Color
// true, false, white, blackThis isn't just a metaphor---it's a precise correspondence. The laws of algebra (commutativity, associativity, distributivity) apply to types too.
We can now describe complex data with precision: AND bundles things together, OR represents alternatives. With these two operations, we can model nearly any domain.
Types are our first line of defense. They define what's possible, catch mismatches, and guide us when code evolves. We saw how Maybe and Result can even encode the possibility of absence or failure directly in the type.
But types cannot catch everything. A type can say "this function takes a Move"---but it cannot know whether the move is legal in the current position. A type can say "this function returns a File"---but it cannot guarantee the file exists on disk. Some rules depend on runtime values, on the state of the world, on things that only become known when the program actually runs.
What happens then? When a move is illegal? When a file doesn't exist? When the network fails? In the next part, we confront the boundaries of what's possible---and how systems handle the impossible.