Imported from TimPapler/dotfiles (
claude/skills/programming-philosophy/SKILL.md). Install upstream withnpx skills add TimPapler/dotfiles --skill programming-philosophy. Copyright stays with the author.
Programming Philosophy
The unifying idea: you should be able to see what the machine is doing. If a language feature, abstraction, or framework hides the machine's behavior from you, it's suspect. If it reveals behavior more clearly, it's good.
Running alongside it: compression-oriented programming — write the code inline first, then factor out repetition after it appears, never before. Abstractions emerge from duplication you can actually see; you don't design them up front and fit code into them. (It's why Before Creating an Abstraction waits for the third use, and why structs emerge from procedures rather than the reverse.)
Priority Hierarchy
When principles conflict, follow this order:
- Correctness — does it produce the right result?
- Simplicity — is it the simplest correct solution?
- Robustness — does it keep running when things fail?
- Performance — only after measuring, and only where it matters
- Generality — only after 3+ real use cases demand it
One thing overrides this ordering: never pessimize. "Performance comes after measuring" defers optimization, not care: needless copies, allocs in loops, one item threaded through five layers are waste, and avoiding waste is never premature. Canonical treatment: Don't Pessimize under the Before Optimizing tree.
Decision Trees
Before Creating a Type (class, protocol, actor)
Do I have data that naturally belongs together?
├─ No → don't create the type
├─ Yes → use a struct (value type)
└─ Do I need reference semantics / shared identity?
├─ No → keep the struct
└─ Yes → use a class, but question why you need shared state
└─ Do I need a protocol for this?
├─ I have 2+ concrete implementations RIGHT NOW → yes
└─ I might need one later → no. Add it when you do.
When you do make one, prefer a single fat struct. Put everything you care about in one struct, even if you suspect you'll outgrow it. Splitting a fat struct later is easy and mechanical — "it was doing this; now it's these two." Re-merging logic you fragmented across many tiny types up front is the hard direction, usually not worth the dig-out. Fat structs hold up fine until the instance count gets large: tens or hundreds, never think about it; past ~10k of them, reconsider the layout (the concrete moves — SoA, side tables for rare fields, narrow indices — are in references/performance.md, Data Layout).
Sanity-check a representation by imagining the harder version. Unsure whether a field's type is right? Ask what the harder problem would need. You'd need a float z to place things in real 3D — so a float z is fine for 2D too, and it lets you animate it. An integer z-index buys nothing and blocks animation; it's a framework hack, not your operation. This applies only to zero-cost representation choices — a field's type, a unit, a coordinate space. It never justifies adding structure (types, protocols, parameters, indirection); those still wait for real uses (see Before Creating an Abstraction).
For a closed set of variants, reach for a tagged enum, not a class hierarchy. When a value is "one of N known kinds" — a shape that's a circle or a rect or a line; a token that's a number or a string or a paren — model it as a Swift enum with associated values and switch over it, not a protocol with N conformers. Every case sits in one place you can read top to bottom; adding a case makes the compiler point you at every switch that must now handle it; and the hot path pays no per-element dynamic dispatch — a switch is branch-predictable and stays inlinable where a vtable call is an optimization barrier. Polymorphism earns its keep when the set of kinds is genuinely open — callers you don't know add their own. For a set you control, the "replace the switch with subclasses" advice trades a visible, fast decision for a scattered, slow one.
Before Creating an Abstraction (protocol, generic, wrapper)
How many times does this pattern appear in real code?
├─ 1 time → inline it. No abstraction.
├─ 2 times → note it. Still inline. Watch for a third.
├─ 3+ times → now compress. Extract the common pattern.
└─ Is the abstraction simpler than the duplication?
├─ Yes → extract it
└─ No → keep the duplication. Wrong abstraction is worse.
Compress by meaning, not by resemblance. The third occurrence is a prompt to compress, not an order — first ask whether the three are the same thing or just the same shape. Code that looks alike but means different things is incidental duplication; merge it and you've welded together two ideas that must now change in lockstep, which is exactly how the wrong abstraction is born (and why it's worse than duplication — duplicated code you can edit independently). When you do extract, the parameters of the new procedure should be the genuine axes along which the copies differ. If you find yourself passing a flag that just selects "which unrelated thing to do," you compressed a coincidence, not a meaning — split it back apart.
When you do extract, the result must be deep: an interface much smaller than what it hides. An extraction whose parameter list is as wide as the code it replaced is a shallow module — it satisfies the rule of 3 while making things worse; inline it back or find the boundary where the interface gets small. (Ousterhout: the best modules hide a lot of functionality behind a simple interface.)
Before Adding Error Handling
Is this a system boundary? (user input, network, file I/O, external API)
├─ Yes → validate, handle failure, degrade gracefully
└─ No → is this an internal call between your own code?
├─ Yes → trust it. No defensive checks.
└─ It's a framework/OS call
└─ Can it fail in a way that matters?
├─ Yes → handle it: log, use a fallback, keep running
└─ No → don't handle it
When things fail, degrade, don't crash. No elaborate error type hierarchies — just: did it work? Log details for debugging, use a fallback, keep running.
But first, reduce the number of things that can fail — that count is an architectural choice, not a fact of the problem. A thousand individual allocations is a thousand failure points; one block allocation for a group that shares a lifetime is a single failure point, checked once (canonical: Lifetime Is a Group Property). The strongest version designs the failure out entirely: return a usable zero stub instead of a failure the caller must handle (Zero Is Initialization). Handle the failures that genuinely remain; delete the handling for the ones you architected away.
Assert your beliefs — assertions are not error handling. assert/precondition detect programmer error, not runtime failure, so they sit outside degrade-don't-crash: a violated internal invariant means the program is already wrong, and crashing loudly in development is executable documentation, not a defensive check. The waste "trust internal calls" deletes is defensive branching that handles the impossible case; the assertions that state why it's impossible stay. Discipline (from TigerBeetle's TIGER_STYLE): assert both the positive and the negative space — the valid/invalid boundary is where bugs live; pair assertions, checking the same property where it's produced and where it's consumed; write assert(a); assert(b), not assert(a && b) — a failure should point at one belief. And put a limit on everything: every loop, queue, and buffer gets an explicit upper bound — "how big does the group get?" hardened into a rule, and what makes up-front allocation possible. (TIGER_STYLE also caps functions at 70 lines; this skill explicitly disagrees — see the long-function stance below.)
Before Optimizing
Have you measured and found this is actually slow?
├─ No → stop. Write it simply. Measure later.
└─ Yes → what's the bottleneck? (classify the rung — see references/performance.md)
├─ Memory layout / cache misses → restructure data (SoA, flatten, pack, pad for SIMD)
├─ Allocations → move to stack, pre-allocate, reserveCapacity, arena/bump allocate
├─ ARC / abstraction overhead (retain/release traffic, witness tables, CoW copies)
│ → remove the boxing: concrete types, visible specialization, in-place mutation
│ (see references/performance.md → Know the Language's Cost Model)
├─ Crossing a boundary (disk/network) → batch, cache, prefetch, fewer round trips, async
├─ Unnecessary work → skip it (don't compute what you don't need)
└─ Algorithmic → change the algorithm
Don't Pessimize. That "stop, measure later" branch is about optimization — and it is the single most misread line in performance work. It does not mean "write it carelessly and clean up later." Two different activities hide under one word: optimization is making a specific, measured thing faster (defer it until you've measured); non-pessimization is simply not doing obviously wasteful work in the first place — and you do that always, from the first line. Copying a buffer you could reference, allocating inside a loop that could reserve once, routing one item through five layers, re-deriving a value you already held in hand: none of that is "optimization you'll get to later," it's just waste, and avoiding it is never premature. This matters because slow software is rarely one hot spot a profiler points you at — it's waste smeared evenly across everything (a flat profile), accumulated one un-thought-about line at a time. So: measure before you optimize; never wait for a measurement to stop pessimizing.
Before Adding a Dependency
Can the standard library do this?
├─ Yes → use standard library
└─ No → how much of this dependency will I actually use?
├─ A small fraction → write the part you need yourself
└─ Most of it → add it, but know the cost (build time, binary size, update burden)
Code Smell → Fix Table
| Smell | Fix |
|---|---|
| Protocol with 1 concrete implementation | Delete the protocol. Use the concrete type directly. (Exception: a seam at a genuine system boundary whose test fake is the second implementation — see Testing Under This Philosophy.) |
| Protocol + N conformers for a fixed, known set of kinds | Tagged enum + switch. All cases in one place; the compiler flags every switch when you add one; no per-element dispatch. |
| Class that could be a struct | Make it a struct. Default to value types. |
| Class wrapping another class | Flatten. Remove the wrapper. |
Method that doesn't use self |
Make it a free function. |
| One linear operation chopped into many tiny functions to hit a size limit | Keep it one function, read top to bottom. Extract only what's reused or genuinely separable. |
Default parameter value that selects behavior (animated: Bool = true, retryPolicy: .default) |
Remove — make the call site say it. Defaults that supply the neutral element (standard alignment, an identity transform) are fine. |
Property wrapper in core logic (@Published, custom wrappers) |
Replace with explicit mutation + notification. Make side effects visible. |
Escaping closure capturing self |
Pass the data explicitly instead of capturing. |
weak self / unowned self dance |
Rethink ownership — the graph is too complex. Often the objects fighting over it share one lifetime; pool them and free together. |
Generic with 3+ where constraints |
You're type-system programming. Simplify or use concrete types. |
| Protocol extension with default implementation | Move the implementation to the concrete type. No spooky action at a distance. |
Deeply chained optionals a?.b?.c?.d |
guard let early. Make the nil case explicit. |
| Error type hierarchy (5+ error cases) | Keep exactly the cases callers branch on differently (usually 2–3: retryable / fatal / expected-absent); collapse everything handled identically. Details go in the log. |
| Manager/Service/Provider/Coordinator class | What does it actually do? A bag of unrelated verbs with no data of its own → inline the work into the call sites. Owns a collection and runs passes over it → it's really the group (see Warning Signs): keep it, named after the collection — LiveEnemies, not EnemyManager. |
| Async function that doesn't need to be async | Remove async. Make it synchronous. |
| Actor (or queue) serializing data only one thread ever touches | Delete the synchronization — partitioned data needs neither. Actors are fine as the single owner of a genuinely shared resource: isolation in the type signature is visible and compiler-checked. |
| Callback / delegate chain 3+ levels deep | Flatten. Direct call from origin to destination. |
| Abstraction with "Base" or "Abstract" in the name | Delete it. Put the code in the actual types. |
| Two code paths merged into one because they look alike, though they mean different things | Incidental duplication. Split them back; let them change independently. |
| Extraction whose parameter list is as wide as the code it replaced | Shallow module. Inline it back, or find the boundary where the interface gets small. |
| Converting the domain model into the algorithm's input shape every call | Store it in the algorithm's shape; convert once at the boundary. |
| Many short-lived allocations with the same lifetime on a hot path | Arena / bump-allocate; reserveCapacity; reset all at once. |
Linked objects each allocated/freed on their own (ARC graph, shared_ptr, new/delete pairs) to manage lifetime |
If they share a lifetime, allocate them as one pool/array and free it together — ownership tracking disappears. |
| Raw index stored across frames into a pool that reuses slots | Generational handle: index + generation, compared on dereference; stale → the zero stub. |
| try/catch or nil-check guarding every small allocation | Symptom, not safety. Collapse the allocations into one block → one failure point; delete the rest. |
Returning nil/throwing so every caller must branch on absence |
Return a zero stub the caller uses without checking; make all-zero the valid empty case (Zero Is Initialization). |
Constructor/deinit chains just to set up and tear down validity |
Make all-zero memory the valid empty value; skip the init/teardown passes (ZII). |
| Per-element branch inside a hot numeric loop | Make it branchless (mask/select/min/max); let it vectorize. |
| Disk/network call inside a loop | Hoist it out, batch it, reduce round trips. One slow boundary crossing, not N. |
| Integer field you'll end up animating (e.g. a z-index) | Make it a float; sort/snap at the end. The int was a framework convention, not your operation. |
| Work deferred into a node tree that a general algorithm walks later | Do it now in imperative code. Defer only what must cross a boundary in time. |
| A general "engine" that grows a new case every time a caller wants something | You're rebuilding a language inside your data. Let the caller write imperative code instead. |
Encapsulation boundary drawn around an on-screen object (Card, Deck) |
Draw boundaries around operations. One dragCard reaches across card and deck data. |
| Event bus / broadcast as the default way parts react | Default to one function doing the N things directly. Broadcast only when listeners are genuinely open-ended. |
Code Is Procedurally Oriented
Code is procedures that do work. "Objects" are constructs that arise from procedures — data bundles that let procedures be reused. Don't design objects first and then write methods inside them. Write the procedures, and let the data groupings emerge naturally.
- Don't start by designing types and relationships. Start by writing the code that does the work.
- Structs emerge when you notice the same group of data being passed around together — that's just convenient bundling, not "object design."
- Functions should operate across bundles of things, not be locked inside one type.
- If a function only touches one type's data, it can be a method. Otherwise, make it a free function.
- Operator overloading is good for math types (vectors, matrices, colors) —
a * b + cis more honest thanadd(multiply(a, b), c) - Comments carry what code cannot: why, units, coordinate spaces, invariants. Code says how; a comment repeating the how is noise, one capturing the why is load-bearing.
A long function that does one linear thing is fine — often better than fragmenting it. "Functions should be small" is not a law. Chopping a single operation into ten tiny functions so each fits a line limit scatters one readable top-to-bottom story across the file and forces whoever reads it to chase calls and rebuild the order in their head. Extract a piece when it's genuinely reused, or when it's a self-contained sub-step whose name earns its place — not to hit a size target. The thing that should be small is the idea, not the line count; a function that does one thing is allowed to be long if that thing is long.
Encapsulation: At Natural Boundaries Only
Hiding an implementation behind a clear API boundary is a useful technique — when there's a natural line of separation. But most code within a program should be miscible. Parts genuinely depend on each other, and forcing artificial separation into "objects" where there are no natural boundaries makes code harder to maintain, not easier.
Don't try to "prevent mistakes" if you're also preventing good, maintainable code. Making a coding mistake in the name of preventing a potential future coding mistake is still a mistake. Apply encapsulation where a real boundary exists, leave code open and miscible everywhere else.
Boundaries Are Operations, Not Pictures
When you see a card and a deck on screen, the instinct is to make a Card object and a Deck object and ask "who needs to know when a card is dragged?" — as if both are encapsulation boundaries that have to be notified. They aren't. There is one operation: drag-card. Write it as one function, called from the mouse handler, that reaches directly across the card and deck data and mutates them. The card doesn't "know" anything — it's data you modify while dragging.
This is the real content of "functions operate across bundles": the unit of structure is the operation, not the noun you can point at on screen. (You can do this in OOP too — the object would be DragCardOperation, not Card. The boundary just wouldn't match the picture.) The precise diagnosis (Muratori, The Big OOPs, 2025): the mistake was never objects or encapsulation — it's a compile-time hierarchy of encapsulation that matches the domain model. And the assembled alternative has a searchable name: ECS — entities as IDs, components as SoA pools, systems as passes over them — which is exactly this skill's group thinking + handles + boundaries-around-operations, shipping in real products since the late '90s.
Why it wins: a real drag does fifty things — spawn sparkles, check proximity to other cards, swell the neighbors, play a sound. One function weaves them together cleanly and they all happen this frame. Spray them across a conspiracy of objects reacting to events and you get a ballet of notifications: far more code, harder to coordinate, often a frame late.
Event bus / broadcast notification is a tool, not a default. Reach for it only when the listeners are genuinely open-ended and unknown to you. For the cases you do know — "the two cards next to this one move aside" — just write the six lines. An if is how you say "do this, not that"; don't trade it away for a broadcast that forces everyone to reconvene to do what you could have said directly.
Imperative Over Deferred
The deepest reason immediate mode beats retained mode isn't about UI — it's about where the work lives. Your language already gives you the full power of code: variables, loops, if, ordering, the ability to build bespoke control flow in exactly the sequence the work needs. The moment you push that work into a data structure that some general algorithm walks later, you throw the power away and have to rebuild it inside the data: loops become sub-arrays, ordering becomes a DAG, conditionals become flags the engine interprets. You reimplement your language, badly, inside your nodes — just so a later pass can re-derive the order you already knew when you wrote the code.
So: do the work now, in imperative code, wherever you can. Defer only what genuinely must cross a boundary in time — state this frame hands to next frame, or one subsystem hands to another later. Everything else, just do it.
The Node-Tree Trap
The seductive pattern: dump everything a caller might want into a node, then write one general algorithm that runs over the nodes and "does the right thing." It never stops growing. Every new thing a caller needs becomes a new node field and a new case in the algorithm, until you've built a Turing-complete language inside your layout engine. That's how you get the DOM.
The tell: the caller can't ask "where is the letter T in this string?" or "put this halfway between those two things" without you adding an engine feature. In imperative code those are trivial — 0.5 * (a + b), ten seconds, done. In the node engine they're a new node type, a new case, and a fresh worry about whether the things you depend on have even been laid out yet. (A hand-written parser is typically a few thousand lines; a parser-generator toolchain with its own DSL, an order of magnitude more. Same trap.) And the engine forces everything through itself, because there's nowhere else for behavior to go — the caller never gets to just do something at the right moment.
Make the Strategies Callable Now
Going imperative doesn't cost you reuse — you get it through ordinary functions. Take the strategies the engine would have run (layoutHorizontal, centerIn, spaceEqually) and make them callable directly, right now, not only from a deferred pass. Then write layoutCard() as plain imperative steps that call them and return a finished (or partially finished) card you can capture and hand to layoutHand(). Small bespoke flows compose; nothing is forced into a universal interpreter.
The test for whether the design is right: can the caller use the information at the call site, right now? If yes, you have what you want — whatever bookkeeping happens under the hood (begin/end state vs. passing params) is an implementation detail. If no, you've trapped the caller.
Across the Frame Boundary, Use Last Frame's Truth
Rebuilding the whole UI every frame is fine — computers are fast and you're touching tens of elements. The one thing you genuinely must defer is the chicken-and-egg of input: to know whether the mouse is over an auto-laid-out element you need its position, which you only have after laying out. So decide hover/hit using last frame's layout and the mouse position as it was when you drew that frame. The guiding rule: every decision the UI makes must be consistent with what was actually on screen. Decide against a position the user never saw and they feel the off-by-one as lag. (The user is always at least a frame behind anyway — buffering, HDMI, TV motion-smoothing. There is no instantaneous frame; aim for self-consistent, not perfect.)
It's Contextual
Not absolute. Regularized UIs — a settings menu that's eleven identical sliders — genuinely benefit from the data-driven bucket: uniform things, easy extra passes. Bespoke UIs — games, anything where every element responds differently and the variety is the point — get strangled by it. Match the approach to the work. And never tear down working code to adopt this. Keep what works; add the imperative option alongside it. Two genuinely different versions of a thing is not a sin — and refactoring working code toward a marginally better shape mid-stream is a real cost, not a free win.
Where a host framework owns the paradigm, conform at the rim. SwiftUI views, UIKit lifecycle, and their property wrappers are retained-mode by fiat; fighting the framework in its own layer buys nothing. Write idiomatic framework code in the thinnest possible shell, and apply this philosophy to the core the shell calls into — the shell/core line is a system boundary in the error-handling sense. The smell-table rows about @Published, view classes, and result builders apply to the core, not to the rim the framework mandates.
Think About the Group, Not the Individual
Your program almost never operates on one thing. It operates on collections of things. Design for the collection — the single-item case is just N=1.
The default instinct is to ask "what is a thing?" and model a rich individual object. Instead ask: "I have N of these. What do I actually do with all of them?"
Why This Matters
- Reveals the real operations. When you think about one enemy, you imagine 15 methods. When you think about 1000 enemies, you realize your program does maybe 4 things per frame: move them, check collisions, update AI, render them. The group strips away imaginary complexity.
- Data layout follows naturally. If you're updating positions for 1000 enemies, you want positions contiguous — not interleaved with sprite data you're not touching. That is the one case to choose SoA while writing: you already know the loop touches a field subset across large N, which makes it a representation choice, not optimization. Otherwise default to AoS — the single layout rule lives in references/performance.md, Data Layout.
- Algorithms simplify. "This enemy needs to find the nearest other enemy" is awkward as a method — it needs a reference to the world. "I have a list of positions, I need nearest-neighbor queries" is just a data structure problem. Clean, no coupling.
- Lifecycle becomes explicit. Individual-object thinking hides questions about creation and destruction behind constructors and destructors. Group thinking forces you to confront: "How do things enter and leave this collection?" The policy is visible, not hidden.
How to Do It
Start from the verbs, not the nouns. List what your program actually does each frame/tick/request. Each operation acts on a group. That's your real design.
Write the loop first:
for i in 0..<count {
// what data do I need here?
}
The body tells you your actual data requirements. The struct emerges from the loop, not the other way around.
Think in passes. One pass to move everything. One pass to check collisions. One pass to render. Each pass is simple, touches minimal data, and is easy to reason about. This is the opposite of "one object does everything to itself."
Name the groups explicitly. Don't just have Enemy and assume a list somewhere. Name the collection as a first-class concept: "the live enemies," "the pending requests," "the dirty tiles." When you name the group, you ask the right questions: how big does it get? What order matters? How do things enter and leave?
Warning Signs You're Thinking Individually
| Sign | What it means |
|---|---|
| A method that needs a reference to its container, context, or "the world" | The operation belongs at the group level |
| You're writing the word "Manager" or "Registry" | If it owns the collection, that's the group — start there and name it after the collection (LiveEnemies). If it's verbs with no data of its own, it's the smell-table row: inline it |
| A method that loops over siblings | Group-level work squeezed through a keyhole |
State like isProcessed, needsUpdate, dirty |
Batch bookkeeping leaking into individual objects |
| You're reaching for Observer, Visitor, Mediator patterns | These solve problems caused by individual-object thinking |
| More types than operations | Over-invested in modeling individuals; flip it — start from operations |
Every object is a class you allocate and free on its own (ARC graph, shared_ptr, new/delete pairs) |
You're tracking lifetime per-item; find the set that shares a lifetime and pool it |
weak/unowned, retain cycles, deinit-ordering puzzles |
The runtime bill for individual-lifetime thinking — collapse to one pool |
| A try/catch or nil-check around every small allocation | Many failure points; one block allocation for the group has one |
Lifetime Is a Group Property
Group thinking isn't only about operations and layout — it applies just as hard to lifetime. The default instinct is to give every object its own: allocate it when it's born, free it when it dies, track that span individually. In Swift this is the path of least resistance — every class instance is a reference-counted individual, and ARC is a fleet of automatic smart pointers threading retain/release through a graph you can't see. The C++ form is new/delete pairs, shared_ptr, and RAII destructors; the Rust form is the borrow checker. All the same stage of thinking: this one thing owns that one thing, and I will track when each dies — and the whole game is getting out of it. The tell that you're still in it: thousands of individual allocations, a rat's nest of pointers from things to other things, and ownership as a constant low-grade concern.
The move: stop asking "when does this die?" and ask "what set of things all die together?" Almost always the things whose lifetimes you were tracking one-by-one actually share a lifetime — born around the same time, garbage around the same time. Allocate them as a group into one pool / arena and free the whole pool at once. Now nobody owns anybody; the lifetime belongs to the pool, and it's obvious rather than tracked.
Smart pointers, RAII, and the borrow checker are patches over the flaw, not the fix. If object A is wired to auto-free when B frees — exactly what a shared_ptr or a deinit chain buys you — then A and B had a combined lifetime all along, and the honest structure is one allocation they both live in. The teardown crawl (free one node, which frees the next…) is runtime work rediscovering a group you knew statically when you wrote the code; keep the group and the crawl disappears.
The concrete win is that failure points collapse. A character has some bones, some meshes, and each mesh some vertices. The individual version allocates the character, then an array of bones, then an array of meshes, then each mesh's vertices — six or eight separate allocations, each one a place the code can fail, each needing a check. The group version writes down everything the character needs up front — this many bones, meshes, vertices — allocates one block big enough for all of it, and parcels out the pointers into that block. Eight failure points become one; the code is eight times less likely to fail there, and you delete the error handling that guarded the other seven. Do this everywhere and the defensive paranoia — try/catch around everything, nil-checks at every step — mostly evaporates, because it was a symptom of the structure: a thousand micro-allocations genuinely is a thousand things that can fail, so the paranoia was correct given the architecture. Change the architecture and the need for the handling goes with it (see Before Adding Error Handling).
The endgame is the per-frame arena: one block of scratch per frame, everything transient allocated by bumping an offset into it, the whole thing reset at frame end. It erases thousands to hundreds of thousands of individual allocations and nearly every failure point with them, because you can bound the size and seed the block once. (Arena mechanics, caveats, and the Swift how-to are in references/performance.md — there it's framed as a speed win; here the point is architecture and failure points. Both are true.)
Leaving stage N in Swift, concretely:
- Prefer value types stored contiguously (
[T]/ContiguousArray) over graphs ofclassinstances. An array is a pool — the elements share its lifetime and die in one deallocation, not N. - Refer to things by index or a small ID into that array, not by reference. IDs don't keep anything alive, need no
weak/unowned, and don't touch ARC. (A flat array of value types addressed by ID is also far easier to serialize and to harden — much harder for an adversary to corrupt than a sprawl of pointers.) - When the pool reuses slots, use a generational handle, not a raw index. A raw index stored across frames into a slot that's been freed and reused is a dangling pointer without the crash — it silently resolves to the wrong live object. Handle = index + generation counter packed together; the slot's generation increments on destroy; dereference compares generations and hands back the zero stub on mismatch (see Zero Is Initialization). Handle-to-pointer conversion is transient: re-resolve at each use, never store the pointer.
- The
weak self/unowneddance, retain cycles, anddeinit-ordering puzzles are the runtime bills for individual-lifetime thinking. Paying them is the signal to find the shared lifetime and collapse to a pool.
Zero Is Initialization: Design the Failure Out
RAII, but good. Instead of running constructors to establish validity and destructors to tear it down, arrange things so all-zero memory is already a valid, usable value — the empty / absent case. Fresh pages from the OS arrive zeroed on any real platform (you pay only the first-touch fault the OS charges anyway), so a new block needs no init pass from you. A reused block does: a per-frame arena's reset just rewinds an offset — it does not zero — so ZII values allocated from a reused arena need the block re-zeroed on reset. A bulk memset is still far cheaper than per-object init, but it is a real pass; the arena in references/performance.md takes zeroed: for exactly this. And ZII is for trivial value types only: an all-zero bit pattern is not a valid Swift value for class references, existentials, or many enums with payloads — the same POD restriction as arenas.
The payoff is stubs. When an operation can't produce a real result — an arena that couldn't grow, a lookup that missed, an ID that doesn't exist — don't return nil and force every caller to branch. Return a shared all-zero stub. Reads from it read zero; writes go into scratch that's cleared before it's handed out again — one stub per thread/frame, or successive users read each other's writes. The caller never checks whether it got a stub; it just uses what it got, and the zero case does nothing. getEntity(0) returns a zero entity, not nil, and everything downstream just works.
This is the strongest form of degrade, don't crash: the failure path isn't handled, it's designed out. A growable arena that can't get more memory hands back a stub; the frame still renders, it just can't draw the thing that didn't fit. No exception, no propagation, no crash — and no error-handling code, because there's no error left to handle.
- Make zero the meaningful default.
Optionalforces a branch at every use; a zero value that behaves as "empty" doesn't. Prefer types whose all-zero bit pattern is the sensible neutral element. - For the rare variably-sized stub (the caller wants N bytes and you can't give N), hand back a pointer to a single ring-mapped page — the same page mapped repeatedly, the way
mmapcan — so reads and writes of any size are harmless and crash-free but accomplish nothing. The fixed-size case is simpler: one shared zero value. - The point isn't zero per se — it's that the fallback is a real value the code path already handles, so there's no separate failure path to write, test, or get wrong.
Silent to the user, never to the developer. A stubbed failure emits no log, no value, no frame — which starves Empirical Debugging of anything to observe when the miss is actually a bug. So make stub issuance observable by rule: count every stub handed out, or debug-log once per frame; degradation stays invisible on screen, visible in the tooling.
Where the line sits — stub vs. explicit result. Stub when silently-do-nothing is the correct degradation: an overflow arena that can't grow (draw nothing this frame), getEntity on a despawned ID, an out-of-bounds tile read. Explicit result when the caller acts on the failure: a save-file write, a purchase, anything the caller retries, reports, or refuses to proceed without.
This principle unifies several other ideas: SoA data layout is what happens when you think about the group's memory (when the loop touches a field subset — the layout rule is in references/performance.md). Immediate-mode APIs are what happens when you think about group operations per frame. "Functions operate across bundles" is just another way of saying the function serves the group, not the individual. And treating lifetime as a group property — one pool born and freed together instead of a thousand individually-owned objects — is the same move applied to memory: it's what collapses smart pointers, ownership tracking, and most error handling into nothing.
Performance
Performance is its own discipline — load it when you're actually optimizing, not before. The full treatment lives in references/performance.md:
- Architecture Follows the Work — find where the bulk of the work is, design for batch, and let the optimal hot loop dictate how the layers above feed it.
- Know the Cost Model — the latency ladder (cache → RAM → SSD → network); classify which rung a bottleneck sits on before touching it.
- Know the Language's Cost Model (Swift) — what ARC, exclusivity, existentials, unspecialized generics, escaping closures, and CoW actually cost, and what each looks like in a Time Profiler trace.
- Estimate the Bound First — napkin-math the speed-of-light floor (bytes ÷ bandwidth) and compare to reality before profiling; a 10–1000× gap is the normal starting point, not an anomaly.
- Data Layout — AoS vs SoA, and preparing data so the inner loop can be SIMD-ized.
- Memory & Allocation — arenas / bump allocation in Swift, and when they actually pay off.
- Measuring Performance (Swift / iOS) — signposts, a profiling harness, the quick timer, Instruments, why it stays off CI, and production telemetry.
The entry point is the Before Optimizing tree above: measure first, classify the rung, then fix. Don't open the reference until you've measured.
API Design
Start from the Call Site
Write the code you want to write at the call site first. Then make the API match it.
Build the Consumer Before the Engine
A library or engine is a boundary, and building it first draws that boundary when you understand the problem least. Write the consumer first — not as scaffolding to justify a library you've already decided on, but instead of the library. Keep the logic inline in the consumer and let the shared piece precipitate out once a second and third real consumer force it. An interface derived from a real consumer expresses what the caller wants (intent); one back-derived from the implementation leaks how it works (mechanism) — the same failure as exposing a state machine or an opaque handle, one level up.
It's a timing rule with two edges. Front edge, consumer #1: don't extract yet. Back edge, consumer #N: once you know — not guess — there are several real consumers, build the shared substrate for all of them; refusing is just duplication rediscovered N times (see Before Creating an Abstraction). Too early buys a premature, leaky boundary shaped by its first caller; too late smears one operation across N call sites.
Strongest as a single-author discipline, where crossing the not-yet-drawn boundary costs ~zero and you extract the instant it's ready. Build the engine first across an engine team and a consumer team and you don't just freeze a code guess — you freeze it into an org boundary, which you can almost never walk back.
Each Call Does Meaningful Work
// Too granular — caller manages state machine
renderer.begin()
renderer.setColor(.red)
renderer.setPosition(x, y)
renderer.drawRect(w, h)
renderer.end()
// Too opaque — what does this do? all of it?
renderer.renderScene(scene)
// Right level — one call, clear batch of work
renderer.pushRect(Rect(x, y, w, h), color: .red)
Immediate Mode Over Retained Mode
Prefer APIs where the caller tells the system what to do each frame/tick, rather than building up a persistent object graph the system manages. No "API state" to sync, no lifecycle to manage, caller is in control. The deeper rationale — and how to keep this from collapsing back into a node-tree engine — is in Imperative Over Deferred above.
No Granularity Discontinuities
Never supply a high-level function that can't be trivially replaced by a few lower-level functions that do the same thing. If your API has a convenience that does A+B+C, the caller must also be able to call A, B, C separately and get the same result. If the high-level function does something the lower-level pieces can't replicate, the caller is trapped — they either use it exactly as-is or they're stuck.
Build APIs in composable layers: low-level primitives that do real work, and high-level conveniences that are just thin combinations of those primitives. The caller can always drop down a level when they need more control.
No Opaque Handles
Pass data directly. Let the caller own the storage. Structs over classes. Data over tokens.
Few Concepts, Many Combinations
One flexible primitive beats ten specialized types. Push specialization to the caller, not into the API taxonomy.
Flat, Not Deep
caller → function → work (good)
caller → manager → service → provider → adapter → work (bad)
Caller Owns the Data
The caller provides memory, the API fills it. No hidden allocations, no ownership ambiguity.
Concurrency Principles
Before Adding Concurrency
Is the work data-parallel (same operation over N items)?
├─ Yes → partition; each thread owns its slice; no sharing, no locks
└─ No → is it one shared resource (a cache, a file, a connection)?
├─ Yes → one owner; others submit work at batched sync points
└─ No → is it just slow I/O?
├─ Yes → async at the boundary; stay serial everywhere else
└─ No → don't add concurrency
On mobile, the main thread is sacred. ~16.6 ms per frame (8.3 ms at 120 Hz); one synchronous disk or network call there drops frames. Every boundary crossing moves off the hot path (cost model: references/performance.md).
1. Don't Share Mutable State
Partition data so each thread owns its slice. If threads don't share, they don't need synchronization.
2. Synchronize at Known Points, in Batches
When threads do need to communicate, do it at well-defined moments — not scattered throughout the code. Threads do independent work, meet at a sync point, exchange results, diverge again. Few sync points, each clearly visible.
3. Avoid Hidden Synchronization
Every mutex, lock, or serialization point should be visible in the code. If a language feature introduces synchronization you can't see, it's hiding the most important thing about concurrent code: where threads wait on each other.
4. Threading Should Be Visible
If you can't tell which thread runs a line of code by reading it, the abstraction is hiding too much.
Build Time Awareness
Fast iteration cycles improve everything. Avoid language features that destroy compile times:
- Complex generic signatures with multiple
whereconstraints - Result builders (especially nested SwiftUI view builders)
- Heavy use of type inference on complex expressions
- Large files with many interdependent types
When compile time regresses, find the cause and fix it. Don't just accept slow builds.
Empirical Debugging
A bug is a wrong belief about what the code does. You don't dislodge it by thinking harder about the code you imagined writing — you find where reality diverges from the belief, by observing. Debugging is measurement, not reasoning.
- Reproduce it first. A bug you can't trigger on demand, you can't fix — you can only guess and hope. Find the smallest input that reproduces it before you change anything.
- Design for reproduction. Whether reproducing is cheap or heroic is an architectural property, not luck: route all randomness through one seedable generator, take time and I/O as inputs rather than ambient globals, and record inputs (strokes, touches, frames) so a failure replays from a log plus a seed. Log the seed with every failure. The perf harness in references/performance.md doubles as the replay vehicle — and single-owner, sync-at-known-points concurrency is what makes deterministic replay possible at all.
- Read the actual value; don't assume it. The whole class of "but it should be X" bugs dies the moment you print /
po/ breakpoint and look. Assume nothing you haven't observed this run. - Bisect the cause. The bug lives between "known good" and "known bad" — in the code path, the commit history (
git bisect), or the input. Binary-search it; each test halves the space, so ~20 steps covers a million lines or commits. - Change one thing at a time. Two changes at once and you can't tell which moved the needle — and you may have masked the first bug while adding a second.
- It's rarely where you first look. If you've stared at the obvious suspect for 15 minutes, your model is wrong somewhere upstream. Widen the search; check the assumption you didn't think to question.
- See it fail, then see it stop. If you can't watch the bug happen — a log line, a value, a captured frame — you can't know you fixed the mechanism, only that it stopped reproducing for now. Confirm the cause, not just the disappearance of the symptom.
Same loop as performance work: form a hypothesis, observe reality, let the observation correct the model. The measurement tooling that does the observing — signposts, Instruments — is in references/performance.md.
Testing Under This Philosophy
The rules above delete the mainstream test seams — single-implementation protocols, injected mocks — on purpose. What replaces them:
- Value-type cores are tested directly. Pure functions over values need no seams: feed real data, assert on outputs. Most of the program should be testable this way precisely because it's built from value types and free functions.
- System boundaries are the one legitimate seam. The same boundaries from the error-handling tree (network, disk, external APIs) are where a protocol or closure parameter earns its place so tests can substitute a fake — no violation of the single-implementation smell, because the fake is the second implementation.
- The harness generalizes. The scaffold that stands a subsystem up alone with representative data (references/performance.md, Build the Harness) is the same artifact that runs correctness fixtures. Too entangled to harness is too entangled to test — the same finding.
- Test with real data at representative N. Fixture files over hand-built object graphs; the group is the unit under test, and N is part of the input.
Written to Be Reviewed
Most code is now drafted by an LLM; the scarce resource is reviewer attention, not typing. Consequences:
- If a generated diff isn't obviously correct at reading speed, regenerate it a different way — don't annotate it. Tokens are cheap; archaeology on output nobody can fully explain is not. Never keep a hand-patched diff you couldn't rewrite from scratch.
- Strip LLM-typical defensive boilerplate deliberately. Needless try/catch, nil-guards on trusted internal calls, redundant validation — the smell table already bans them; generated output is where to apply it hardest.
- Reviewability is a first-class output of this style. Procedural, flat, visible-machine code is exactly what makes a diff cheap to verify — state it as a goal, not a side effect.
Review Checklist
Simplicity
- Simplest correct solution?
- Any abstraction without 3+ real uses?
- Any code that can be deleted?
- Any wrappers that add nothing?
- Any protocol with only 1 implementation?
- Any compression that merged things that merely look alike (incidental duplication)?
- One operation fragmented into tiny functions just to hit a size limit?
- A protocol + N conformers where the set of kinds is closed (use a tagged enum + switch)?
- Work deferred into a node tree when imperative code could just do it now?
- A general "engine" growing a new case per caller request?
- Any extraction that's a shallow module (interface as wide as what it hides)?
- Is the diff obviously correct at reading speed? If not, restructure or regenerate — don't annotate.
Group Thinking
- Am I modeling an individual when I should be modeling the collection?
- Any methods that need "the world" or a manager to do their job?
- Does each operation touch all fields, or just a few? (data layout hint)
- Can I describe what this code does as "one pass over N things"?
- Are encapsulation boundaries drawn around operations, or around on-screen objects?
- Broadcasting events where one function could do the N things directly?
- Am I tracking lifetime per-object when a set of things share one lifetime and could be a single pool?
- Any ownership machinery (
weak/unowned,shared_ptr,deinitchains) that grouping the lifetime would delete?
API Boundaries
- Caller in control? (owns data, chooses order, provides buffers)
- Opaque handles that could be plain data?
- How many concepts must user learn?
- How many layers between caller and work?
- Can the caller use the result at the call site right now, or is everything trapped behind a deferred pass?
Robustness
- App keeps running when things fail?
- Failures logged for debugging?
- Elaborate error types that all get handled the same way?
- Did I reduce the number of failure points architecturally, or just wrap each one in handling?
- Any defensive check guarding a failure the architecture already makes impossible?
- Could a zero stub (ZII) replace a nil-branch or a hand-written failure path? (Counted or debug-logged, so degradation stays visible to the developer.)
- Do internal invariants have assertions, in both positive and negative space?
- Does every loop, queue, and buffer have an explicit upper bound?
Performance
- Any pessimization I can drop right now without measuring? (needless copy, alloc inside a loop, item routed through layers, value re-derived)
- Do I know where the bulk of the work actually is?
- Does the hot path process the batch, or one item at a time through layers?
- Is data already in the shape the hot algorithm wants, or converted every call?
- Inner loop SIMD-friendly? (contiguous, branchless, tail padded; SoA where it touches a field subset — see the layout rule)
- Unnecessary allocations? Could an arena /
reserveCapacitycollapse them? - Boundary crossings (disk/network) batched, cached, and off the hot path?
- Which rung of the cost model is this stuck on? (memory / boundary / compute)
- Measured in Release on a real device (not Debug / simulator), debugger detached?
- Is the hot region signposted so the quick timer, the harness, and Instruments all read the same thing?
- Reporting a distribution (min / p50-p90-p99), warmed up — not the mean of a cold, noisy sample?
Concurrency
- Can you tell which thread runs each line?
- Data partitioned so threads don't share?
- Hidden synchronization points?
Debuggability
- Can you see what this code does when it runs?
- Can you step through it in a debugger?
- Can you reproduce the failure on demand before attempting a fix?
Workflow
Writing New Code
- Understand the problem — what exactly needs to happen?
- Write the simplest thing that works — linear, obvious code
- Make it robust — degrade, don't crash
- Test with real data
- Measure if performance matters
- Compress if patterns emerge — only after 3+ real uses
Designing APIs
- Start with the call site — what does the caller want to write?
- Build a real consumer before extracting the engine/library — inline first, extract only at 3+ real uses
- Make the common case trivial
- Caller owns the data — no hidden state, no opaque handles
- One concept, many uses — push specialization to the caller
- Flat, not deep — minimize layers between caller and work
Optimizing
- Measure first — find the actual bottleneck (signpost it; baseline in Instruments on a real device)
- Understand the cost model — what's expensive and why? Which rung is it stuck on?
- Consider data layout — often the biggest win
- Make targeted changes — one variable at a time, iterating against the harness via the quick timer
- Measure again — confirm the win on device against the harness, record the before/after numbers, and watch production telemetry for fleet-wide regressions