1 Overview: The Whole Argument in Miniature
The central question is simple:
What knowledge can the model carry so that the rest of the program no longer has to rediscover it?
The answer is to define meaningful values, establish their contracts at the right boundaries, and compose operations that preserve or deliberately transform that meaning. The following ten theses summarize the argument.
1.1 1. Type-driven design makes domain knowledge explicit
Type-driven design is a way of structuring thought: identify domain concepts, admissible states, relationships, and operations, then represent them in the values and contracts from which the program is composed. The mechanism in R is runtime construction, validation, and dispatch, but the consequence is larger than catching bad values. A named DateRange can retain the fact that its endpoints are ordered; a raw pair of dates forces every consumer to infer which is first, decide whether equality is allowed, and repeat the check.
S7 provides nominal classes, properties, validators, and generic dispatch for carrying this knowledge. A useful model is therefore measured not by how many classes it creates, but by how much rediscovery it removes. See Types and Meaning.
1.2 2. Nouns and verbs are co-designed
In S7’s generic-function tradition, classes describe domain nouns and generics describe domain verbs. Methods belong to generics rather than being stored inside classes, so behavior is organized around operations that many types may support. That mechanism changes the design process: a model is not finished when its property graph looks plausible. Asking how values will be render()ed, combine()d, prepare()d, or serialized often exposes a distinction the nouns missed; a newly distinguished noun may in turn reveal a clean dispatch axis or a law shared by several operations.
A good design therefore iterates between representative values and the operations consumers need. Nouns without verbs become inert schemas; verbs without meaningful nouns become branches over loose data. Co-design lets each side correct the other. See Object Systems.
1.3 3. Choose the substrate by semantics
S7 is useful for domain values with invariants and dispatchable behavior. vctrs is useful for homogeneous vectors that must slice, recycle, and inhabit data-frame columns. R6 objects and environments are useful for shared identity and live resources. S3 remains effective for lightweight interoperability and for designs driven by an existing external registry. The choice determines which promises are natural: vector algebra, nominal contracts, or shared identity.
Real systems therefore combine substrates instead of choosing a camp. ellmer’s R6 Chat owns an ongoing conversation, network activity, and an accumulating lifecycle; its S7 Turn, Content, Model, and Provider objects carry inspectable records and descriptions through that process. The contrast is memorable because each system does the job its semantics fit. S7 must earn its place through meaning, invariants, operations, or composition, not merely through formality.
1.4 4. Separate editable values, read-only snapshots, and reference owners
An editable S7 value changes through replacement: an operation returns a revised value while other bindings ordinarily retain the prior one. This makes branching and history explicit—a draft can become two candidate drafts without either candidate silently changing the other. A read-only snapshot is also a value, but its public API intentionally offers no revision path because it records a completed fact. A reference owner has shared identity, a live resource, or an observable lifecycle, so aliases are expected to observe the same change.
These are three lifecycle policies, not synonyms for mutable and immutable. A draft document may be an editable value, its published form a snapshot, and the editor coordinating both a reference owner. Copy-on-modify does not make every S7 property read-only, and wrapping an environment, R6 object, or external pointer does not turn the payload into a value. See Values and State.
1.5 5. Put each contract at the smallest boundary that can state it
Property definitions carry intrinsic shape, cardinality, membership, bounds, and local presence policy. Class validators carry relationships among one object’s properties. Aggregate roots carry rules over contained objects and references. Contextual operations carry checks that require datasets, metadata, resources, or authority. The mechanism follows information visibility: a scalar property can know that alpha lies in [0, 1], but only a style aggregate can know that every layer names an existing source, and only an operation with runtime metadata can know that the source still exists.
Placing each rule once lets higher layers trust lower ones and keeps one predicate from drifting across constructors, methods, and serializers. Construction certifies only the invariants visible at that boundary and preserved by supported transformations; it is a bounded certificate, not a claim of omniscience.
1.6 6. Construction should retain knowledge
A useful boundary does more than reject bad input. Rejection says only “not this”; retention lets later code rely on what was established. A DateRange records ordered endpoints, explicit candidate values record tuning intent that would otherwise be guessed from vector shape, and a prepared run can record completion of data-dependent checks. The consequence is fewer repeated questions: possession of the richer value answers them.
This is the dynamic-language form of parse, don’t validate, with an important qualification. Strict construction of known values can coexist with tolerant parsing when uncertainty is represented explicitly as an unknown or partial variant with diagnostics. Retaining uncertainty is still retaining knowledge; silently collapsing it to a familiar value is not.
1.7 7. Alternatives, families, and failure protocols are separate designs
An S7 union states which alternatives a property accepts, and those alternatives may include primitive or domain classes. Concrete classes preserve variant identity. An abstract parent supplies shared schema or a dispatch protocol, but does not seal the family. These mechanisms solve different problems: acceptance, identity, shared behavior, and extension. Coverage tests and loud fallbacks help detect unsupported cases; neither proves exhaustiveness.
Failure adds a second axis that is easy to hide. A Success/Failure outcome family represents what happened, but it does not decide what happens when several outcomes are combined. One workflow may stop on the first failure, another may accumulate all diagnostics, and an interactive boundary may turn the same failure into an R condition. Modeling the failure shape and the composition protocol separately prevents a sum type from smuggling in one global control-flow policy.
1.8 8. One meaning may have several deliberate projections
An authoritative declaration can drive a native validator, schema, documentation, UI description, or serializer. The mechanism is projection: one owned semantic specification is interpreted into several artifacts. The consequence is not infallibility but a smaller agreement problem. Instead of maintaining five independent definitions, tests can check that each generator faithfully projects the one source.
This makes generation from an owned semantic specification a legitimate architecture. Blindly generating a class graph from incidental foreign structure is different: it imports someone else’s arrangement without first deciding which distinctions have meaning in the R API.
1.10 10. General machinery must earn its keep; meaningful concepts need not
Every abstraction introduces a new place to look. It earns that indirection when it absorbs a real axis of change: generic-to-method dispatch absorbs behavioral variation across types, a semantic class prevents meanings with the same primitive representation from being confused, and a wire-name map contains serialization churn. A pass-through wrapper or speculative factory with one use merely moves code without removing a decision.
The rule of three is therefore a useful restraint on generalized factories, metamodels, and framework layers. It is not a requirement to leave an important domain concept unnamed until it appears three times. Start from the domain and intended consumers, sketch representative values and operations, record the contracts encountered, and extract reusable machinery only from real repetition. A large flat class or generated facade is not wrong merely because it has many fields; the question is whether its shape is authoritative and whether it makes its consumers simpler. Abstractions pay rent by deleting rediscovery, branching, duplication, or coordinated change.
The remaining chapters develop these theses through mechanics, public package examples, decision guides, and executable laws. The Doctrine compresses them for design reviews.