Modularity: dividing a program into parts
On this page
A program that lives long enough changes, and the hardest part of a large one is keeping a change from spreading. The answer is to divide it into parts, each with a job and a boundary, so that a change stays inside the part it concerns and a part written once can be used again. This part of the manual is about where to draw those boundaries and what Cone gives you to draw them with. Afterwards you can split a program into modules and packages, decide what each part shows, and build parts that can be swapped, reused and extended.
Here is one small program with boundaries at four sizes: a block, a function, a type and a trait.
mod tally;
import stdio use *;
// A type: its state, and the methods that are its interface.
// count has no pub, so no code outside this module can reach it
struct Tally {
count i32;
pub fn add(self &mut, n i32) {
count += n;
}
pub fn total(self &) i32 {
count;
}
}
// A trait: whatever has a total method may stand in for it
trait Totalled {
pub fn total(self &) i32;
}
// A function: its parameter and its workings are its own
fn report(what &<Totalled) {
print <- "total: ", what.total(), "\n";
}
fn main() {
var days = new Tally(0);
// A block: n belongs to the loop, and is gone when it ends
each n in 1 .. 10 {
days.add(n);
}
report(&days);
}
It prints:
total: 55
Try this: change how Tally keeps its count, say by counting how many times add is called as well. Nothing outside the struct needs to change, and report never knows. That is the whole idea of this part, in miniature.
Why divide a program#
Dividing a program into parts serves two needs. The first is insulation from change: when the way a part works has to change, the change should stay inside it. The second is reuse: a part written once should be usable somewhere else, by another part of the program or by another program. Both rest on a third idea, isolation: a part keeps its state and its workings to itself, so that nothing outside it comes to depend on them. What nothing outside could reach, nothing outside can be broken by.
How well this works depends on where the boundaries are drawn. Count the dependencies among the pieces inside a part, its cohesion, and the dependencies that cross from one part to another, its coupling. A good division has much of the first and little of the second. The everyday way to get there is to separate concerns, one responsibility to a part. But it can be overdone: a program cut into too many small parts pays more in coupling between them than it saves in complexity inside each, so the goal is the fewest boundaries that keep each part's concerns together.
A language that supports all of this well is a larger language than one that does not. What it gives back is reuse and versatility, and programs that are easier to reason about and to debug.
The layers of a program#
A program is divided at several sizes at once, each layer built from the one below. Some layers are about logic, the work being done, and some about data, the state it is done to:
| Logic | Data | Where Cone has it |
|---|---|---|
| Block | Tuple | Blocks, and tuples on Structs |
| Function | Type | Functions, structs and enums |
| Thread | Actor | Actors, and Concurrency |
| Program | Package | Modules and packages, in this part |
In the small, at blocks and functions, programming languages largely agree on how a part is made and kept to itself. The further down the table, the less they agree. Cone's approach is to give each mechanism the same shape at every layer where it appears, so that what you learn for a type carries over to a module, and what you learn for a module carries over to a package.
Six strategies, each building on the last#
The mechanisms for dividing a program cluster around six strategies. Each builds on the ones before it, so they are worth knowing in order:
- Composition. A part is assembled from smaller parts, which it owns: a block from its statements, a struct from its fields, an enum from its variants, an actor from its state and behaviours, a module from its files and submodules, a package from its modules. Layer under layer, the parts form a tree. A with block (in Recipes) works on one value's parts without repeating its name.
- Namespaces. Each part is given a name, unique within the part that holds it, by which it is reached from outside: geometry.Point, room.x. Modules covers names across modules and inside types.
- Isolation, or encapsulation. A part publishes its interface and hides how it is made, which shrinks the surface others can couple to. A function's locals are its own; a struct's private fields are its module's; a module's names are private unless it says pub. Public and private has the rules.
- Substitution. One part may be swapped for another that meets the same interface, the way any bulb of the right fitting goes into a light socket. Between parts, Cone's subtyping is this: a struct stands in for a trait it meets, an enum's variant for its enum, a function for a function reference of its signature. Subtyping covers why that is safe (and the smaller substitutions, of numbers and references), and Traits the interfaces themselves.
- Generativity. Many concrete parts are made from one template, by putting parameters into it: generics and macros.
- Extensibility. A part gains usable pieces without being changed: a type reaches a part's names as its own, reuses another type's capabilities, or adds to a type somebody else wrote. Cone ties these together as name folding.
The same shape at every layer#
The table shows where each strategy is found at each layer. Read across a row, and the mechanism is recognisably the same one each time:
| Block and function | Type | Module | Actor | |
|---|---|---|---|---|
| Composition | a block's statements | fields and variants | a folder's files, submodules | its state and behaviours |
| Namespaces | local variables | a type's members | modules and imports | |
| Isolation | locals private to their block | private members | private names | private state |
| Substitution | function references, closure references | traits, virtual references | module traits | actor trait (not built) |
| Generativity | generic functions, macros | generic types | generic modules | a generic actor (not built) |
| Extensibility | a field's names folded in, enrichment, enum extension | a global's names folded in, a module extending a module |
The spelling follows the mechanism, not the layer. The same few words do the same job for a type and for a module:
| What it says | For a type | For a module |
|---|---|---|
| these names in there are now names in here | pub engine Engine use fuel, thrust; | import math3d use Point3, Mat3; |
| built out of another of its kind | struct Gauge extends Meter | mod solids extends shapes; |
| meets an abstraction | struct Node is Id | mod plain is Runner; |
| generic over a type | struct Box[T] | mod stack[T]; |
How this part is laid out#
It starts with the decisions every program larger than a page has to make, and goes deeper from there:
- Modules: files, submodules and imports: dividing a program into modules, and reaching one module's names from another.
- Public and private: what a module, and a type inside it, shows to the rest of the program.
- Packages: a module compiled on its own and shared, and the rule that dependencies run one way.
- Global state and initialization: a module's own variables, and the order in which every module is set up and taken down.
- Name folding: reaching a part's names as your own, reusing another type's capabilities, and extending a type you did not write.
- Subtyping: when one type may stand in for another, and what the swap costs.
- Traits: interfaces, and the parts that meet them, settled while the program is compiled or while it runs.
- Function and closure references: behaviour handed around as a part.
- Generics and macros: one template, many versions.
The same strategies come back at the actor's layer in Concurrency, and a module's boundary with code written in C is in Talking to C.
You now have the map: the layers, the strategies, and where each is in this part. The first boundary almost every program needs is the module, so that is where we start.