Decomposition and Composition

IterationHigher-Order Functions

A chess game is complicated. There are pieces, positions, moves, captures, checks, castling, en passant, promotion, stalemate... How do you build something so complex?

You don't. You build small things. Then you combine them.

This is the fundamental strategy for managing complexity: decomposition (breaking big things into small things) and composition (combining small things into big things).

The Problem of Complexity

Imagine trying to write a chess game as one giant piece of code. Every rule, every piece, every special case, all tangled together. It would be:

  • Unreadable: Too much to hold in your head
  • Unmodifiable: Change one thing, break ten others
  • Untestable: Can't isolate parts to verify
  • Unreusable: Nothing can be extracted for other uses

The antidote is decomposition: identify separate concerns and handle each independently.

Procedural: Procedures and Modules

The procedural approach decomposes by naming sequences of commands:

sql
    procedure validate_move(board, move):
        if not is_valid_position(move.from):
            return false
        if not is_valid_position(move.to):
            return false
        piece = get_piece(board, move.from)
        if piece is empty:
            return false
        if not can_piece_reach(piece, move.from, move.to):
            return false
        return true

Notice how validate_move delegates to smaller procedures:

  • is_valid_position
  • get_piece
  • can_piece_reach

Each of those might decompose further. We build a tree of procedures, each handling one concern.

Functional: Functions and Pipelines

The functional approach decomposes by defining small transformations:

elixir
    valid_moves(board, player) =
        board.pieces
        |> filter((p) => p.owner == player)
        |> flat_map((p) => moves_for_piece(board, p))
        |> filter((m) => is_legal(board, m))

The power is in composition---the |> operator:

python
    // These small functions...
    get_player_pieces(board, player)
    moves_for_piece(board, piece)
    is_legal(board, move)
    
    // ...compose into complex behavior
    valid_moves = get_player_pieces 
        >> flat_map(moves_for_piece) 
        >> filter(is_legal)

Object-Oriented: Objects and Collaboration

The object-oriented approach decomposes by distributing knowledge and behavior:

wollok
    class Knight:
        valid_moves(board):
            // Knight knows its own movement rules
            return l_shaped_moves_from(position, board)
    
    class Board:
        pieces_for(player):
            // Board knows where pieces are
            return pieces.filter((p) => p.owner == player)
        
        is_legal(move):
            // Board knows legality rules
            return not causes_self_check(move)
    
    class Game:
        valid_moves():
            // Game orchestrates
            player_pieces = board.pieces_for(current_player)
            all_moves = player_pieces.flat_map((p) => p.valid_moves(board))
            return all_moves.filter((m) => board.is_legal(m))

This is distributed responsibility. Instead of one procedure knowing everything, knowledge lives where it belongs.

What Makes Good Decomposition?

Across all paradigms, good decomposition shares qualities:

Single Responsibility: Each unit does one thing.

python
    // Bad: does too much
    validate_and_apply_move_and_update_history(...)
    
    // Good: separate concerns  
    validate_move(...)
    apply_move(...)
    record_in_history(...)

Clear Boundaries: Inputs and outputs are explicit.

python
    // Bad: relies on hidden global state
    validate_move()      // what board? what move?
    
    // Good: explicit parameters
    validate_move(board, move)

Appropriate Abstraction Level: Not too detailed, not too vague.

python
    // Too low: implementation details leak
    move_piece_from_array_index_to_array_index(12, 28)
    
    // Too high: what does this even mean?
    process_game_action(thing)
    
    // Just right: clear intent, hidden details
    move_piece(board, e2, e4)

Composition Patterns

Composition is how small things become big things:

Sequential Composition: Do this, then that.

wollok
    // Procedural
    result = step1(x)
    result = step2(result)
    result = step3(result)
    
    // Functional
    result = x |> step1 |> step2 |> step3
    
    // OOP
    result = processor.step1().step2().step3()

Nested Composition: Outer thing contains inner things.

python
    // Procedural: nested calls
    result = outer(middle(inner(x)))
    
    // Functional: same
    result = outer(middle(inner(x)))
    
    // OOP: object graphs
    game.board.square(e4).piece

Conditional Composition: Choose which path.

python
    // All paradigms have branching
    result = if condition then path_a(x) else path_b(x)

The Fractal Nature

Decomposition is fractal---it applies at every scale:

  • A system decomposes into services
  • A service decomposes into modules
  • A module decomposes into classes/functions
  • A class decomposes into methods
  • A method decomposes into statements

The skill is knowing when to decompose and when to stop. Too little decomposition: tangled complexity. Too much: scattered confusion.

We've seen how to break things apart and put them back together. But there's a related question: what can each part see? This leads us to our next building block: scoping---the art of controlling visibility.