Cone Language Reference

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:

LogicDataWhere Cone has it
BlockTupleBlocks, and tuples on Structs
FunctionTypeFunctions, structs and enums
ThreadActorActors, and Concurrency
ProgramPackageModules 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Generativity. Many concrete parts are made from one template, by putting parameters into it: generics and macros.
  6. 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 functionTypeModuleActor
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 saysFor a typeFor 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:

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.