Scoping and Boundaries

Decomposition

In a game of chess, each player sees the whole board. But in a large software system, not everything should see everything. A function calculating a pawn's moves shouldn't need to know about the game's undo history. The UI shouldn't directly manipulate the board's internal representation.

Scoping is the art of controlling what can see what. It's about drawing boundaries around information.

Why Hide Things?

Imagine every part of your program could access and modify every other part. Chaos:

python
    // Anywhere in the code...
    game.board.squares[37].piece.has_moved = false  // Cheating!
    game.internal_move_count = 0                     // Reset the game!
    game.history.clear()                             // Erase the past!

Information hiding creates islands of certainty. Within a boundary, you control what happens. Outside, you present only what others need to know.

Procedural: Block Scope

In procedural languages, scope is defined by blocks---regions of code delimited by braces or keywords:

sql
    procedure make_move(board, from, to):
        piece = board[from]         // 'piece' exists from here...
        
        if piece.kind == pawn:
            bonus = 10              // 'bonus' only exists in this block
            score = score + bonus
        
        // bonus doesn't exist here
        board[to] = piece           // ...to here
    
    // piece doesn't exist here either

This is lexical scoping: where a name appears in the text determines where it's visible.

python
    // Loop variables are scoped to the loop
    for i in 1..8:
        for j in 1..8:
            process(board[i][j])
        // j doesn't exist here
    // i doesn't exist here

Functional: Closures

Functional languages extend scoping with closures---functions that capture their environment:

sql
    make_counter():
        count = 0                   // private to this closure
        
        increment():
            count = count + 1       // can see 'count' from outer scope
            return count
        
        return increment
    
    counter_a = make_counter()
    counter_b = make_counter()
    
    counter_a()  // returns 1
    counter_a()  // returns 2
    counter_b()  // returns 1 -- separate 'count'!

Closures create private state without objects. The captured variables are:

  • Invisible from outside
  • Persistent across calls
  • Independent for each closure instance

Object-Oriented: Encapsulation

Object-oriented programming makes visibility explicit with access modifiers:

wollok
    class Piece {
        var hasMoved = false         // private: only Piece methods see this
        var position
        
        method moveTo(square) {      // public: anyone can call this
            position = square
            hasMoved = true          // internal update
        }
        
        method canCastle() = !hasMoved   // public query, private data
    }

The object's public interface is what it exposes. The private implementation is how it works. The boundary between them is the contract.

python
    // Outside code:
    piece.moveTo(e4)           // OK - public method
    piece.hasMoved = false     // ERROR - private variable!
    
    // The object controls its own state

Module Boundaries

Beyond individual functions and objects, we have modules---larger units of organization:

wollok
    // Wollok: a singleton object as module
    object chessEngine {
        var internalBoard            // private
        var moveHistory              // private
        
        // Public API - what others can use
        method newGame() { ... }
        method makeMove(move) { ... }
        method validMoves() { ... }
        method gameState() { ... }
    }

Modules create larger-scale boundaries:

  • Package in Java
  • Module in JavaScript/Python
  • Crate in Rust
  • Namespace in C++

The Principle: Minimum Visibility

Across all paradigms, a principle emerges: expose only what's necessary.

wollok
    // Bad: everything exposed via 'property'
    class Game {
        var property board
        var property history
        var property players
        var property currentTurn
        // ... exposing all internals
    }
    
    // Good: narrow public interface (Wollok)
    class Game {
        var board            // private
        var history          // private
        
        method makeMove(move) { ... }  // public
        method validMoves() { ... }    // public
        method isOver() { ... }        // public
    }

Benefits of minimal visibility:

  • Freedom to change: Private details can evolve
  • Easier reasoning: Less to consider
  • Fewer bugs: Less accidental interference
  • Clearer contracts: Public API is the truth

Boundaries at Different Scales

Scoping operates at every level:

ScaleBoundaryExample
ExpressionBlocklet x = 5 in x + 1
FunctionParameters/localsfunction f(x) { let y... }
ObjectPrivate/publicprivate has_moved
ModuleExportsmodule.exports = {...}
ServiceAPIREST endpoints
SystemNetworkFirewall rules

Information vs Behavior

What should boundaries hide?

Hide data representation:

python
    // Expose behavior, not structure
    board.piece_at(e4)              // Good: what you need
    board.squares[4][3].contents    // Bad: exposes internals

Hide algorithms:

python
    // Expose intent, not mechanism
    moves.sort_by_quality()         // Good: what you want
    quicksort(moves, quality_cmp)   // Bad: exposes how

Hide decisions that might change:

python
    // Today: store moves in a list
    // Tomorrow: maybe a tree?
    // Clients shouldn't know or care

Scoping controls what can see what. But there's a related pattern: what happens when one piece of code needs another to do something? This is delegation---the art of asking for help without doing the work yourself.