Scoping and Boundaries
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:
// 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:
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 eitherThis is lexical scoping: where a name appears in the text determines where it's visible.
// 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 hereFunctional: Closures
Functional languages extend scoping with closures---functions that capture their environment:
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:
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.
// Outside code:
piece.moveTo(e4) // OK - public method
piece.hasMoved = false // ERROR - private variable!
// The object controls its own stateModule Boundaries
Beyond individual functions and objects, we have modules---larger units of organization:
// 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.
// 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:
| Scale | Boundary | Example |
|---|---|---|
| Expression | Block | let x = 5 in x + 1 |
| Function | Parameters/locals | function f(x) { let y... } |
| Object | Private/public | private has_moved |
| Module | Exports | module.exports = {...} |
| Service | API | REST endpoints |
| System | Network | Firewall rules |
Information vs Behavior
What should boundaries hide?
Hide data representation:
// Expose behavior, not structure
board.piece_at(e4) // Good: what you need
board.squares[4][3].contents // Bad: exposes internalsHide algorithms:
// Expose intent, not mechanism
moves.sort_by_quality() // Good: what you want
quicksort(moves, quality_cmp) // Bad: exposes howHide decisions that might change:
// Today: store moves in a list
// Tomorrow: maybe a tree?
// Clients shouldn't know or careScoping 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.