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.

yaml
    Piece:
        color: Color      // AND
        kind: PieceKind   // both present

This 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:

sql
    Point:
        x: Number AND
        y: Number
    
    Person:
        name: Text AND
        age: Number AND
        email: Text
    
    Move:
        piece: Piece AND
        from: Square AND
        to: Square

The 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.

yaml
    SquareContent:
        empty OR
        piece: Piece

This 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:

yaml
    Color:
        white OR black
    
    PieceKind:
        king OR queen OR rook OR bishop OR knight OR pawn
    
    GameResult:
        white_wins OR black_wins OR draw

The 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:

yaml
    ChessState:
        board: Board AND          // has both
        turn: Color

But Color itself is an OR:

yaml
    Color:
        white OR black            // is one of

And each square contains an OR:

yaml
    SquareContent:
        empty OR Piece

Where Piece is an AND:

yaml
    Piece:
        color: Color AND
        kind: PieceKind

The 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: Piece

Now 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 it

The 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:

yaml
    Result:
        success: ChessState OR
        failure: Error

Now 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 error

Pattern 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 + " " + kind

The 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:

python
    // 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: A×BA \times B
  • OR is like addition: A+BA + B
python
    // 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, black

This 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.