diff --git a/README.md b/README.md index 7e8aa777..c9a5772a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,138 @@ +# Context-Generic Programming (CGP) -# `cgp` - Context-Generic Programming in Rust - -[![Apache 2.0 Licensed](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](https://github.com/contextgeneric/cgp/blob/master/LICENSE) +[![Apache 2.0 Licensed](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](https://github.com/contextgeneric/cgp/blob/main/LICENSE) [![Crates.io](https://img.shields.io/crates/v/cgp.svg)](https://crates.io/crates/cgp) +[![Tests](https://github.com/contextgeneric/cgp/actions/workflows/tests.yml/badge.svg)](https://github.com/contextgeneric/cgp/actions/workflows/tests.yml) ![Rust Stable](https://img.shields.io/badge/rustc-stable-blue.svg) -![Rust 1.81+](https://img.shields.io/badge/rustc-1.81+-blue.svg) +![Rust 1.89+](https://img.shields.io/badge/rustc-1.89+-blue.svg) + +**A Rust language extension for reusable, swappable trait implementations — checked at compile time, with zero runtime cost.** + +Context-generic programming (CGP) is a library, built on stable Rust, that lets one interface have many interchangeable implementations and lets each *context* — an application, a test, a deployment — choose which one it uses. The choice is written in one readable place and resolved entirely during compilation, so it compiles down to direct calls: there is no runtime container, no reflection, and nothing left in the binary for an implementation a context does not use. + +> [!IMPORTANT] +> The `main` branch tracks the upcoming **v0.8.0** release (published as `0.8.0-alpha` pre-release crates). The current stable release on crates.io is **v0.7.0**, which is **not compatible** with this branch — v0.8.0 changes and removes syntax that v0.7.0 used. **All documentation in this repository describes v0.8.0 only.** For v0.7.0, refer to its published crate documentation instead. + +**[Website](https://contextgeneric.dev/) · [crates.io](https://crates.io/crates/cgp)** + +## Key features + +CGP's headline promises are few and deliberately concrete. + +- **One interface, many implementations.** Write many interchangeable implementations of the same interface and choose between them per context — including the overlapping and orphan implementations Rust normally forbids, made safe because every choice is explicit and local. +- **Zero-cost abstraction.** Everything is resolved at compile time and compiles down to direct calls, so the flexibility costs nothing at runtime and unused implementations never reach the binary. +- **Type-safe wiring.** All wiring is checked at compile time, so a missing dependency is a build error rather than a runtime failure — in safe Rust, with no `dyn`, `Any`, or reflection. +- **Abstract over every dependency.** Write core logic that names its error type, runtime, and I/O abstractly and let each context supply the concrete choice, which keeps the core `no_std`-friendly — from embedded systems to WebAssembly. +- **Still ordinary Rust.** CGP is a superset of ordinary traits that you adopt one piece at a time: providers read like normal impls and implicit arguments like normal parameters, so a codebase can use it in one corner and stay otherwise vanilla. + +## A quick look + +Real operations depend on infrastructure — a database, object storage — and the usual ways of supplying it do not scale. Passing each dependency as an argument threads long lists through every layer, while grouping them into a struct and adding methods locks the logic to that one concrete type, so it cannot be reused for a test, an embedded build, or a different backend. CGP lets you write the operation once against the capabilities a context provides. + +```rust +use cgp::prelude::*; + +// A context is a plain data struct. +#[derive(HasField)] +pub struct App { + pub database: PgPool, + pub storage_client: Client, + pub bucket_id: String, +} + +// Write the operation once. `#[implicit]` reads `database` from the context; +// `user_id` stays an ordinary argument. Any context carrying a matching +// `database` field gains `get_user` — no wiring, no trait impls to write. +#[cgp_fn] +#[async_trait] +pub async fn get_user( + &self, + #[implicit] database: &PgPool, + user_id: &UserId, +) -> anyhow::Result { + // ...query `database` for the user row +} +``` + +`get_user` reads like an ordinary async function, yet it works on any context with a matching field — checked at compile time, so a context missing `database` is a build error rather than a runtime failure. When a step needs *more than one* implementation, promote it to a component and let each context pick a provider: + +```rust +// One interface... +#[async_trait] +#[cgp_component(StorageObjectFetcher)] +pub trait CanFetchStorageObject { + async fn fetch_storage_object(&self, object_id: &str) -> anyhow::Result>; +} + +// ...with as many implementations as you need... +#[cgp_impl(new FetchS3Object)] +impl StorageObjectFetcher { /* fetch the object from Amazon S3 */ } + +#[cgp_impl(new FetchGCloudObject)] +impl StorageObjectFetcher { /* fetch the object from Google Cloud Storage */ } + +// ...and each context picks one, resolved at compile time. +delegate_components! { App { StorageObjectFetcherComponent: FetchS3Object } } +delegate_components! { GCloudApp { StorageObjectFetcherComponent: FetchGCloudObject } } +``` + +`App` fetches from Amazon S3, and `GCloudApp` — a context carrying a Google Cloud storage client instead — fetches from Google Cloud. Neither pays for `dyn` or runtime dispatch, the choice is a single greppable line, and code that calls `self.fetch_storage_object(..)` never changes. (Method bodies are elided here for brevity.) + +## Installation + +Add the latest stable release with Cargo: + +```bash +cargo add cgp +``` + +then bring everything into scope with a single import: + +```rust +use cgp::prelude::*; +``` + +CGP requires **Rust 1.89+** and runs on the **stable** toolchain — no nightly, no fork. + +> [!NOTE] +> `cargo add cgp` installs the current stable release, **v0.7.0**. The examples in this repository target **v0.8.0**; to follow them today, depend on the pre-release from `main`: +> ```toml +> cgp = { git = "https://github.com/contextgeneric/cgp" } +> ``` +> Once v0.8.0 is released, `cargo add cgp` will install it. + +## When to use CGP — and when not + +CGP is a tool with a boundary, and it earns its cost only past that boundary. The rule of thumb is to **reach for CGP when a capability needs more than one implementation and the choice belongs to the context — not before.** Concretely: + +- **One implementation?** Use a plain trait or function. CGP would be over-engineering. +- **A small, closed set of variants?** Use an `enum` and a `match`. +- **Implementations chosen at runtime** — plugins, config-driven selection, heterogeneous collections? Use `dyn Trait`; CGP resolves everything at compile time and gives up that runtime openness by design. +- **One implementation must be globally unique** — a single `Ord` for a map key, say? A coherent trait is the right tool; CGP's per-context choice is deliberately not global. + +CGP is a *superset* of ordinary traits, so adopting it is a climb from the plainer tool rather than a rejection of it. You can use it exactly where it pays and leave the rest of a codebase unchanged. + +## Documentation + +Learn CGP at the project website, **[contextgeneric.dev](https://contextgeneric.dev/)**, which hosts the guides, worked examples, and project blog. + +> [!NOTE] +> The crates' API docs on `docs.rs` are still sparse. The website is the place to start while the inline documentation is filled in. + +## Project status + +CGP works today on stable Rust and already offers a broad feature set: components and providers, abstract types, extensible records and variants, a handler and computation family, namespaces, and modular error handling. It is also young — the paradigm and its ecosystem are still evolving, and v0.8.0 is a pre-release — so adopting it is a real decision rather than a foregone one. + +Two properties make it low-risk to try. Because CGP is a superset of ordinary traits, it can be introduced in one corner of a codebase and stepped back from without a rewrite; and because it imposes no runtime, it does not lock a project into a framework's lifecycle. If you are evaluating it for a team, start small, and lean on the worked examples on the [website](https://contextgeneric.dev/) to gauge the learning curve honestly. + +## How the project is organized + +CGP is a collection of small, layered crates, arranged so that low-level primitives stay independent of the high-level facade. Most users depend only on the single **`cgp`** crate, which re-exports the core and extra functionality through `cgp::prelude`. Pluggable error backends are opt-in as separate crates — `cgp-error-anyhow`, `cgp-error-eyre`, and `cgp-error-std`. Browse [`crates/`](crates) for the full layout. -## Overview +## Contributing -The `cgp` project contains a collection of micro Rust crates that empowers _context-generic programming_ (CGP), a new modular programming paradigm in Rust. +Contributions, questions, and bug reports are welcome. Open an [issue](https://github.com/contextgeneric/cgp/issues) or a pull request, and see the [website](https://contextgeneric.dev/) to get oriented first. Unless stated otherwise, contributions are made under the terms of the Apache-2.0 license below. -To learn more about context-generic programming, check out the our website [contextgeneric.dev](https://contextgeneric.dev/), and our book [Context-Generic Programming Patterns](https://patterns.contextgeneric.dev/). +## License -[!WARNING] -At the moment, the `cgp` crate its constructs are mostly undocumented within Rustdoc. The best way to understand CGP is to read the book [Context-Generic Programming Patterns](https://patterns.contextgeneric.dev/). +Licensed under the [Apache License, Version 2.0](LICENSE). diff --git a/docs/README.md b/docs/README.md index ab9c35bc..24cf1dc5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,7 @@ The knowledge base also serves as a contract. When an agent changes how a macro ## How it is organized -The knowledge base is divided into five top-level sections, and will grow to contain more as the need arises. +The knowledge base is divided into several top-level sections, described below in no particular order, and will grow to contain more as the need arises. Each section answers a different question about CGP, so a reader picks the one that matches their need rather than reading them in sequence. The [reference/](reference/README.md) directory holds one document per CGP construct — one for `cgp_component`, one for `cgp_impl`, one for `delegate_components`, and so on. Each document is self-contained and explains a single construct completely: its purpose, its accepted syntax, the exact code it desugars to, worked examples, and links to the constructs it relates to. The [reference index](reference/README.md) lists every construct and tracks which ones are documented. @@ -22,6 +22,10 @@ The [guides/](guides/README.md) directory holds the guides to *writing* CGP — The [errors/](errors/README.md) directory catalogs the compiler errors CGP produces *after* codegen — input a macro accepts and lowers to Rust that then fails to compile — organized by the kind of error rather than by the macro that produced it. Each document records the anatomy of one class: the mistake that triggers it, the shape of the diagnostic, whether the root cause is present in the output, and where it sits when it is. The catalog is built around the distinction between errors that *surface* their root cause and those the compiler *hides*, and it is the canonical documentation for the post-codegen compile-fail fixtures, so it exists both to guide debugging agents and to map the ground a `cargo-cgp`-style tool would post-process. Failures a macro raises by *rejecting* its input stay with the macro's implementation document instead. +The [related-work/](related-work/README.md) directory looks outward instead of inward: each document takes an external concept, framework, or language feature that resembles CGP — dependency injection, implicit parameters, and so on — explains it faithfully and with citations, weighs what its users like and dislike about it, and positions CGP against it. These documents exist to serve future user-facing writing, giving an agent who must explain CGP to readers of a particular background the honest comparison and the positioning strategy to build on the intuitions those readers already hold. + +The [communication-strategy/](communication-strategy/README.md) directory turns that outward-facing material into guidance for *writing* about CGP to the public — landing pages, tutorials, articles, blog posts, social-media threads. Where a related-work document compares CGP to one external idea, a communication-strategy document generalizes across those comparisons into audience-level strategy: which readers exist and what each already believes, which content and hooks gain the most attention, which misunderstandings CGP reliably provokes and how to preempt them, and what tone keeps everything written about CGP reading as one voice. An agent preparing any public-facing writing reads here first, then draws on the matching related-work document for the depth of a particular comparison. + ## How to use it An agent working on CGP should read the relevant reference document before changing a construct, and should consult the `/cgp` skill for the conceptual framing that ties constructs together. The reference documents assume familiarity with the vocabulary the `/cgp` skill establishes — consumer traits, provider traits, providers, wiring, and so on — and focus on precise per-construct semantics rather than re-teaching the paradigm. Read the two together: the skill for the shape of the forest, the reference for the individual trees. diff --git a/docs/communication-strategy/AGENTS.md b/docs/communication-strategy/AGENTS.md new file mode 100644 index 00000000..44beca04 --- /dev/null +++ b/docs/communication-strategy/AGENTS.md @@ -0,0 +1,41 @@ +# AGENTS.md + +This file provides guidance to LLM agents when working with code in this repository. + +This directory holds the **communication-strategy** documents of the CGP knowledge base — the guidance an agent uses when writing anything public-facing about CGP, from a landing page or tutorial to a blog post, an article, or a social-media thread. Read the knowledge-base [README.md](../README.md) for the background on the whole base, the section's own [README.md](README.md) for what it covers, and the governing [../AGENTS.md](../AGENTS.md) for the rules every section shares. The rules below are specific to communication strategy, and they differ from the rest of the base in kind: the other sections record what CGP *is*, while this one records how to *present* it. + +## The roles you play here + +When you work in this directory, you are not a coding assistant transcribing facts — you are CGP's **marketing director** and **developer-relations lead**, and you are expected to contribute real expertise in those roles. Bring the judgment those jobs demand: audience psychology, positioning, message discipline, brand voice, community empathy, and an instinct for what earns attention versus what invites a pile-on. Do not defer these decisions or hedge them as "not a technical matter"; they *are* the matter here, and a vague or timid answer is a failure of the role the same way a wrong expansion is a failure in a reference document. Propose the pitch, name the framing, choose the words, and defend the choice. + +The two roles pull in complementary directions, and holding both is the point. The **marketing director** asks what makes CGP worth a reader's attention: which capability to lead with, which audience a piece targets, which one-line framing lands, and how to keep every piece of public writing sounding like one coherent voice. The **developer-relations lead** asks whether a developer will *trust* what they read: whether a claim survives contact with an expert, whether a cost is being hidden, whether the tone respects the reader's intelligence, and whether the community will feel courted or condescended to. Marketing without devrel produces hype that the Rust audience punishes on sight; devrel without marketing produces honest writing no one reads. Every document here must satisfy both. + +## What every communication-strategy document must do + +Each document turns audience knowledge into concrete, usable guidance for a future writer, and shares a set of obligations. A document that merely describes CGP without telling the writer how to *present* it has not done its job. + +- **Be prescriptive, not descriptive.** Say what to do: which selling point to lead with for which reader, which objection to defuse first, which exact words to prefer and which to avoid. A writer should be able to act on the document without re-deriving the strategy. +- **Ground every claim about audiences in the evidence.** The sentiment about what developers value and resent — about dependency injection, implicit parameters, type classes, effects, reflection — lives, cited, in the [related-work](../related-work/README.md) documents, and the facts about what the community measurably reads, discusses, and how CGP has actually been received live, cited, in [attention-and-engagement.md](attention-and-engagement.md). Draw on both and link to them rather than inventing reactions; when you assert that "the Rust audience distrusts macro magic" or that "a topic earns attention," it should trace to real, cited sentiment or a real, cited survey, thread, or post. External citations are concentrated in those two homes so the strategy documents stay in one voice; add a new external source there and link to it rather than scattering raw URLs across the section. +- **Keep every CGP claim true.** A selling point that names a capability CGP does not have, or a rebuttal that promises behavior it does not deliver, is the most damaging kind of error here, because it is shown to the audience most able to catch it. Every factual claim about CGP is bound by the [synchronization rule](../AGENTS.md) exactly as a reference document's Expansion is: verify it against the source and the `/cgp` skill, and prefer the modern idioms the skill and [guides](../guides/) teach. +- **Pair advantage with honesty.** Because the eventual reader is often a skeptic, name the cost beside the benefit wherever the audience will look for it. A document that only sells is a document devrel would reject. +- **Write for the marketing-naive expert.** These documents are read as much by human CGP advocates as by agents, and that reader is fluent in CGP but new to marketing, public communication, and developer relations — so calibrate the prose to that exact gap. Explain a non-technical concept the first time it appears, define the term of art, and anchor it in an intuition a systems programmer already holds; but never re-teach CGP itself, because the reader knows the technology far better than the craft. When you invoke a principle like positioning, framing, social proof, or the funnel, treat it as new to the reader and worth a plain sentence of explanation; when you name a CGP construct, treat it as familiar. The concentrated home for these definitions is [glossary.md](glossary.md) — introduce a term inline the first time a document leans on it, but link the glossary rather than re-defining a recurring term in every document, the way external citations concentrate in [attention-and-engagement.md](attention-and-engagement.md). The [README](README.md) states the audience and the general principles this calibration rests on. + +## Honesty is the strategy + +The single rule that governs everything here is that honesty *is* the marketing strategy, not a constraint on it. CGP's public audience is unusually able to detect spin — they are the practitioners of the very concepts CGP compares itself to — so an overclaim, a strawman of a competing tool, or a hidden cost does more damage than saying nothing. The guidance in these documents therefore always leads with a true, concrete capability, states it in the reader's own vocabulary, and concedes the genuine trade-offs, because that is what actually persuades this audience. When a piece of strategy tempts you toward exaggeration, treat the temptation as a signal that the honest version needs a better frame, not that the honest version needs to be abandoned. + +Two guardrails follow directly and are absolute. Never fabricate evidence — no invented benchmarks, adoption numbers, quotations, or version-specific claims; when a number or quote would strengthen a point, either source it or omit it. And never disparage another language, framework, or community to elevate CGP; the related-work documents set the standard of representing every compared tool as its own users would recognize it, and public writing must meet the same bar. Naming where a competing tool is simply the better choice is a devrel asset, not a concession. + +## Keeping the section in sync + +These documents sync against three moving targets rather than one, and a review checks all three. First, they sync against **CGP's actual capabilities**: a selling point or rebuttal that rests on a feature the code no longer has, or misses a capability newly added, is stale and must be corrected — the synchronization rule applies to every CGP claim. Second, they sync against **the related-work sentiment**: community attitudes evolve, features that were experimental ship and features that were praised fall out of favor, so when a related-work document's sentiment is revised, revisit the selling points and skepticisms that rest on it. Third, they sync against **the reader profiles**: [reader-profiles.md](reader-profiles.md) is the audience model the rest of the section builds on, so a change to who the readers are ripples into what to sell them and which objections to expect. + +A fourth sync target is the section's own vocabulary of the craft: a non-technical term of art introduced anywhere earns an entry in [glossary.md](glossary.md) in the same change, with a plain definition and a programmer's anchor, so the section keeps one home for these definitions the way it keeps one home for external citations. When a term's fuller treatment moves or is reworded — the funnel in the [README](README.md), positioning in [positioning.md](positioning.md), the audience-facing word list in [vocabulary.md](vocabulary.md) — check that the glossary's short gloss and its pointer still agree. + +The documents also sync against **each other**, and the tightest coupling is a three-way one: a single CGP claim shows up as a capability in [selling-points.md](selling-points.md), as the objection it provokes in [skepticism.md](skepticism.md), and as the pain it removes in [problems-solved.md](problems-solved.md). These are three views of the same thing — the reader who hears "compile-time dependency injection," asks "isn't DI heavy and magical?", and feels "I can't swap my mock for the real thing without a framework" is one reader — so a claim added to any of the three usually implies an entry in the other two, and an edit to one should check whether its mirrors need the same edit. Keep the wording across all of them consistent with [vocabulary.md](vocabulary.md), which is the consolidated authority on which words to use and avoid: a writer must never be told to prefer a phrase in one document that another warns against, and when the two disagree, the vocabulary list resolves it. [positioning.md](positioning.md) draws the same boundary the "why not just traits" skepticism draws, so keep the two aligned as well. + +## Document structure + +These are strategy documents, not reference documents, so they do not follow the reference template of Purpose/Syntax/Expansion. They follow the dual-reader prose style instead: open with a level-one heading naming the document and a one-sentence summary, open every section and subsection with a self-contained topic sentence, and frame every list with a sentence before it. What is distinctive here is that the writing is *about* wording, so quotable example phrasings are welcome and a short framed list of "say it like this / avoid this" is often the clearest form — use it freely, but frame it, and let the prose around it carry the reasoning. Prefer plain language and the knowledge base's established CGP vocabulary — consumer trait, provider trait, provider, wiring, impl-side dependency, context — so a reader moving between this section and the rest never reconciles two dialects. + +Register every new document in the catalog in [README.md](README.md) in the same change that adds it, and cross-link generously: to [reader-profiles.md](reader-profiles.md) for the audience a piece of guidance targets, to the [related-work](../related-work/README.md) documents for the sentiment and the comparison a claim rests on, to the [concepts](../concepts/README.md) for the CGP idea behind a selling point, and to the [reference](../reference/README.md) for the exact construct a claim names. diff --git a/docs/communication-strategy/README.md b/docs/communication-strategy/README.md new file mode 100644 index 00000000..7e257f8d --- /dev/null +++ b/docs/communication-strategy/README.md @@ -0,0 +1,71 @@ +# CGP Communication Strategy + +This directory holds the communication-strategy documents of the CGP knowledge base — guidance for anyone preparing public-facing material about CGP, from a landing page or a tutorial to a blog post, an article, or a social-media thread. Where the rest of the knowledge base records what CGP *is*, this section records how to *present* it: which ideas to lead with, how to frame the features that will feel unfamiliar, which misunderstandings to head off, and what tone to keep so that everything written about CGP reads as one consistent voice. + +These documents are written by AI agents, but their readers are as much human as machine. The intended human reader is a **CGP advocate** — someone who understands CGP deeply and wants to promote it — and the defining assumption about that reader is that their expertise is lopsided: strong in the technology, and often near-zero in the domains that decide whether the technology is ever heard about. A gifted CGP practitioner is not automatically a good explainer of CGP, and is usually the opposite, for the reason the principles section below explains. So these documents deliberately teach the non-technical craft — marketing, public communication, developer relations — to a reader who has never studied it, spelling out principles a marketer takes for granted and grounding them in terms a systems programmer already trusts. An LLM agent drafting public material and a human advocate promoting the project turn to the same documents for the same reason: to borrow judgment they do not natively have. + +## Why this exists + +CGP adds language-level capabilities that many Rust developers have never seen — a consumer/provider trait split, per-context wiring, impl-side dependencies, type-level tables — and the same true sentence about them can excite one reader and alienate another. A functional programmer hears "overlapping instances made safe" as a gift; a pragmatic Rust engineer hears the same pitch as a warning that the crate is too clever. Public-facing writing therefore succeeds or fails on framing as much as on accuracy, and framing depends on knowing who is reading and what they already believe. This section captures that knowledge once, so an advocate or agent preparing public writing starts from a shared understanding of the audience and a shared set of positioning decisions, rather than re-deriving them for every post — and so the whole body of CGP's public material speaks with one voice. + +The purpose is twofold: to help writing **gain attention**, by naming which content and hooks draw the most interest, and to help it **minimize misunderstanding**, by naming the misreadings CGP reliably provokes and how to preempt them. Both goals run through the reader: attention is won by leading with what a specific audience cares about, and misunderstanding is avoided by defusing the specific misreading that audience is prone to. + +## Relationship to related work + +This section is the natural companion to [related-work/](../related-work/README.md), and the two are read together when preparing public writing. A related-work document explains one external idea — dependency injection, type classes, reflection — faithfully, records what its users like and dislike, and positions CGP against it; a communication-strategy document generalizes across those comparisons into audience-level guidance about which readers exist, what they already believe, and how a piece should be shaped for them. When a related-work document records a sentiment — that Rust developers reach for Dagger to escape reflection's runtime cost, say — this section turns it into a reader trait an author can plan around. Read the matching related-work document for the depth of a comparison; read here for the shape of the audience. + +## Principles from marketing, public communication, and developer relations + +Promoting CGP well depends on a body of knowledge that has nothing to do with CGP, and this section makes that knowledge explicit for a reader who has never needed it. The documents in the catalog apply these principles to CGP's specific case; what follows here are the principles themselves, drawn from marketing, public communication, and developer relations, stated plainly for a technical reader and anchored where possible in an intuition a programmer already holds. None of it is secret or hard — it is simply a separate discipline, with its own rules, that being excellent at CGP does not confer. + +The one principle to internalize before all the others is the **curse of knowledge**: the more completely you understand CGP, the worse your instinct for explaining it to someone who does not. Expertise erases the memory of confusion, so the expert leads with the mechanism they find elegant, uses vocabulary that is precise to them and opaque to everyone else, and skips the motivating problem because it feels too obvious to say out loud. Nearly every failure mode this section warns against traces back to this one, and the correction is always the same: write for the reader's current knowledge, not your own. A CGP advocate is the highest-risk author precisely because they know the most, so treat your own fluency as a bias to correct rather than an advantage to lean on. + +The terms these principles use — and the ones the rest of the section reaches for, from *positioning* and *framing* to *the funnel* and a *call to action* — are collected, with a plain definition and a programmer's analogy for each, in the [glossary of the non-technical craft](glossary.md). Read it as the quick-reference companion to this section: where the principles below are the argument, the glossary is the lookup for any single word in it that is unfamiliar. + +### Marketing: positioning and attention + +Marketing, stripped of its reputation, is the discipline of getting the right idea into the right head in the right words, and its first law is that the reader — not the author — decides what a thing *is*. A reader meeting CGP does not build an understanding from scratch; they pattern-match it, in seconds, to the nearest category they already know ("oh, it's a DI framework," "it's macro magic," "it's academic"), and that snap category, not your careful explanation, is what they remember and repeat. **Positioning** is the deliberate act of choosing that category for them before they choose a dismissive one, which is why the wording of a one-liner matters out of all proportion to its length. A handful of consequences follow and recur throughout the section. + +- **Lead with the benefit, not the feature.** A reader cares about the problem you remove, not the mechanism you use to remove it — a well-worn marketing adage holds that no one wants a drill, they want a hole. For CGP this means opening on a pain a developer already feels, never on the consumer/provider split. +- **Attention is scarce and adversarial.** A reader gives a new project seconds before continuing or dismissing it, so the first line carries most of the message and the rest is read only if the first line earns it. You are not competing with a rival crate for attention; you are competing with the reader closing the tab. +- **Framing decides the reaction.** The same true fact, worded two ways, produces opposite responses — "overlapping instances made safe" delights one reader and alarms another — so the choice of frame is not decoration on the message, it *is* the message. +- **Differentiation answers "why this and not that."** A reader who sees no difference from a tool they already have will keep the tool they already have, so a pitch must name what CGP does that the obvious alternative cannot. + +### Public communication: clarity and consistency + +Public communication is the craft of being understood at scale by people you will never meet, and it rewards nearly the opposite of what technical writing trains. Precision and completeness, the virtues of a specification, are liabilities in a pitch: a reader skimming a landing page needs the one idea that matters, not the exhaustive truth, and burying the point under qualifications loses them. The governing skills are clarity and consistency, and they compound across the whole body of writing rather than living in any single piece. + +- **Write for one reader, not everyone.** A message aimed at "all Rust developers" reaches no one, because the framing that wins the pragmatist loses the enthusiast; naming a single target reader is the first decision in a piece, ahead of the outline. +- **Prefer clarity over completeness.** Say less, clearly. One vivid idea a reader keeps beats five accurate ideas they forget, and an omitted nuance can be added later while a lost reader cannot be recovered. +- **Make it concrete, and make it a story.** People remember a concrete example and a problem-tension-resolution narrative far better than an abstract claim or a feature list, so a before-and-after on real code outperforms any adjective. +- **Keep one voice.** Consistency is what turns scattered posts into a recognizable project: when every piece uses the same words for the same ideas, each reinforces the others, and when they drift, the reader senses incoherence even if they cannot name it. This is why the section fixes a shared vocabulary rather than letting each author invent one. + +### Developer relations: trust and community + +Developer relations is communication with an audience that has been marketed to badly its whole career and has grown expert at detecting it, so its rules invert the naive ones: this audience rewards restraint and punishes hype, and its trust, once lost, is not won back by the next post. Trust is the entire currency, which is why **honesty is not a constraint on the strategy but the strategy itself** — an overclaim, a strawman of a competing tool, or a hidden cost does more damage than saying nothing, because the reader who catches it discounts everything else you say, often loudly and in public. The principles below are what earn and keep that trust. + +- **Show, don't tell — and let peers do the telling.** A developer believes running code and another developer's experience far more than a vendor's adjective, so demonstrate a capability rather than asserting it, and remember that **social proof** — a real system built with CGP, a peer vouching for it — persuades where self-praise cannot. +- **Concede the costs.** Naming a genuine limitation, and naming where a simpler tool is the better choice, is not a weakness in the pitch; with this audience it is what makes the rest of the pitch believable. The advocate who declines to oversell is the one who is believed. +- **Meet developers where they are, and respect their time and intelligence.** Condescension and hype both read as disrespect; the tone that works is a knowledgeable peer explaining something useful, neither talking down nor puffing up. +- **Mind the funnel, and match the ask to the reader's stage.** A reader moves through stages — first hearing of CGP, growing curious, trying it, adopting it, advocating for it — and each stage wants different content and a different next step, so asking a first-time reader to bet a codebase on CGP fails as surely as handing a ready adopter another elevator pitch. +- **Beware the pile-on.** A technical community can turn collectively, and a single post that reads as arrogant or dishonest can trigger a public dunking that outlives it; the defensive move is the same as the honest one — claim only what is true, and never disparage another tool to elevate CGP. + +Taken together, these principles are the foundation the rest of the section stands on, and the catalog below is largely them worked out in CGP's specific case: the [reader profiles](reader-profiles.md) apply "write for one reader," the [selling points](selling-points.md) and [problems solved](problems-solved.md) apply "lead with the benefit," the [skepticism](skepticism.md) and [positioning](positioning.md) documents apply "concede the costs" and "trust is the currency," the [tag lines](tag-lines.md) and [key features](key-features.md) apply "positioning" and "attention is scarce," the [technical barriers](technical-barriers.md) document is the curse of knowledge answered move by move, and the [vocabulary](vocabulary.md) document enforces "keep one voice"; the [worked examples](worked-examples.md) then show every one of these principles operating at once on finished copy. A reader who absorbs the principles here will recognize them everywhere below. + +## The catalog + +The documents below inform how CGP is presented to the public; register a new one here in the same change that adds it. The authoring rules for the section — including the marketing-director and developer-relations roles an agent takes on here — live in [AGENTS.md](AGENTS.md). + +- [Reader profiles](reader-profiles.md) — the kinds of readers public-facing CGP writing must serve, from newcomers to Rust through advanced developers and across the backgrounds they arrive from, with what each reader already knows, is excited by, grows skeptical of, and needs from a piece written for them. +- [Selling points](selling-points.md) — the true capabilities CGP should advertise, each with the phrasings that make it land and the phrasings that backfire, plus audience-tuned one-liners keyed to the reader profiles and the related-work comparisons. +- [Skepticism](skepticism.md) — the objections a reader brings to CGP, whether imported from a paradigm they distrust or native to the Rust community, judged for whether they are justified and answered with wording that convinces without triggering the misunderstanding that fed them. +- [Tag lines](tag-lines.md) — a brainstorm of the candidate one-line descriptions of CGP, from the incumbent "modular programming paradigm" through the "language extension" and feature-first framings, each weighed for attention, skepticism, and honest feasibility, with a recommended shortlist to validate. +- [Key features](key-features.md) — the short, curated headline set of the few best selling points to put on the front page, with titles and one-line copy chosen for breadth, honesty, and the phrasing lessons of the rest of the section. +- [Technical barriers](technical-barriers.md) — the comprehension barriers a reader hits when learning CGP, from unfamiliarity with generics and traits upward, and the design affordances and progressive-disclosure teaching moves that lower each one. +- [Attention and engagement](attention-and-engagement.md) — the evidence base for where the Rust community's attention actually sits, what CGP's own public reception has been, and which conversations and channels a piece should attach to; the engagement counterpart to related-work and the home of the section's external citations. +- [Problems solved](problems-solved.md) — the concrete problems CGP removes, written as short before-and-after stories a writer leads with so a piece opens on a pain the reader already feels; the pain axis that complements the capabilities in selling-points and the objections in skepticism. +- [Positioning](positioning.md) — the honest decision guide for when to reach for CGP and when a plainer tool wins, alternative by alternative, so a piece draws CGP's boundary in public rather than letting a skeptic draw it. +- [Formats and channels](formats.md) — per-artifact playbooks for the launch post, tutorial, README, talk, thread, and comparison, each with its opening move, length, the dismissal to preempt, and the call to action, plus the conversion ladder that matches the ask to the reader. +- [Worked examples](worked-examples.md) — the section's guidance assembled into finished, annotated model drafts of a launch post, a README above the fold, and a social thread, each followed by a note tracing every move back to the document that argues for it; the "show, don't tell" companion that lets a writer copy the moves rather than re-derive them. +- [Vocabulary and message discipline](vocabulary.md) — the canonical word list for public writing: the term to use for each concept, the plain-language gloss that introduces it, the terms to defer, and the words and framings to avoid, so everything reads as one voice. +- [Glossary of the non-technical craft](glossary.md) — plain-language definitions of the marketing, public-communication, and developer-relations terms of art the rest of the section uses, each anchored to an intuition a systems programmer already holds; the quick-reference companion to the principles above, and distinct from the vocabulary list, which governs the words used *about* CGP rather than the words of the craft itself. diff --git a/docs/communication-strategy/attention-and-engagement.md b/docs/communication-strategy/attention-and-engagement.md new file mode 100644 index 00000000..9460c2ba --- /dev/null +++ b/docs/communication-strategy/attention-and-engagement.md @@ -0,0 +1,54 @@ +# Attention and engagement + +This document grounds the section's attention goal in evidence: where the Rust community's attention actually sits, what CGP's own public reception has been so far, and what both facts imply for the hooks and channels a piece should choose. + +## Why the strategy needs engagement evidence + +Attention is an empirical fact, not a matter of taste, so the decision of what to lead with should rest on what the Rust community measurably reads, upvotes, and argues about. This document is the engagement counterpart to [related-work](../related-work/README.md): where a related-work document carries cited sentiment about the *concepts* CGP compares itself to, this one carries cited facts about the *community* CGP is writing for, so a claim about what earns attention traces to a real survey, thread, or post rather than to a hunch. It is therefore the home of the section's external citations — the other strategy documents link here for the evidence behind an audience claim, the way they link to related-work for the evidence behind a concept claim. Community attention shifts, so this document is a sync target in its own right: when the survey or the discourse moves, the hooks that rest on it must be revisited. + +A caution on reading the evidence: public engagement metrics are noisy, and absence of a discussion is not proof of absence of interest. The findings below are strong enough to steer framing decisions, but they are inputs to judgment, not verdicts, and the closing section treats publication itself as the real measurement. + +## What the Rust community worries about — and what it rewards + +The clearest signal of where attention sits is the annual survey, and it names two costs to concede and one benefit to lean on. The [2024 State of Rust survey](https://blog.rust-lang.org/2025/02/13/2024-State-Of-Rust-Survey-results/) reports that slow compile times remain the perennial top pain, that subpar debugging support is among the leading tooling complaints, and that 45.2% of respondents named the language's growing *complexity* as a worry for its future — while, asked to prioritize the project's work, developers ranked runtime performance second only to fixing compiler bugs. Read together, these dictate three moves for a CGP writer. + +- **Concede compile-time cost and verbose diagnostics early and plainly.** They are the community's live sore spots, and a reader is actively scanning a new abstraction for whether it worsens them, exactly as [skepticism.md](skepticism.md) prescribes. A piece that stays silent on them reads as either naive or evasive to the survey's respondents. +- **Treat "this adds complexity" as the most dangerous perception a piece can leave.** Complexity is the community's named fear for the language's future, so "still ordinary Rust," gradual adoption, and problem-first restraint are not merely pleasant framings — they are the direct answer to the audience's stated anxiety, and the reason [key-features.md](key-features.md) reserves a headline slot for "Still Ordinary Rust." +- **Lean hard on zero runtime cost.** The survey shows the community explicitly prizes runtime performance, so "resolved at compile time and compiled to a direct call" lands as an answer to something they already care about, not as an abstract virtue. This is the empirical backing for the [zero-runtime-cost selling point](selling-points.md). + +## The conversations that draw attention + +Some topics reliably draw the Rust community's attention, and a piece that attaches CGP to a live conversation borrows its energy — provided the attachment is honest. Four are worth naming, each with the way CGP connects and the overclaim to avoid. + +- **The orphan rule and overlapping implementations.** This is a durable, recurring frustration, with a repository dedicated to cataloguing its design problems ([Ixrec/rust-orphan-rules](https://github.com/Ixrec/rust-orphan-rules)) and a steady stream of posts on the newtype workaround. It is CGP's strongest attachment point because the pain is concrete and widely felt; lead the working-developer and type-system audiences here rather than on the paradigm. +- **Async and function coloring.** The [function-coloring debate](https://www.thecodedmessage.com/posts/async-colors/) recurs whenever async Rust is discussed, and async `fn` in traits carries its own well-known friction around [`Send` bounds and `dyn`](https://github.com/rust-lang/rust/issues/103854). CGP's handler family and its `Send`-recovery pattern touch this, but the honest attachment is narrow — CGP does not remove function coloring, and a piece must not imply it does. +- **Error handling.** The `anyhow`-versus-`thiserror` question is among the most-written-about topics in Rust, and CGP's abstract error type speaks to it directly. This is a strong, low-controversy hook for the working developer, framed as "abstract over the error type so fallible code never commits to one," per the [abstract-types selling point](selling-points.md). +- **Reflection and compile-time introspection.** This is a live, high-attention, officially-pursued area: `facet` drew wide interest, `bevy_reflect` is established, and the Rust project has a [reflection-and-comptime goal](https://rust-lang.github.io/rust-project-goals/2026/reflection-and-comptime.html) with a landed MVP. CGP's "compile-time structural reflection encoded in types" sits squarely in this conversation, which makes it timely — but because first-class compile-time reflection is coming to the language itself, position CGP as *available today* and *type-level and checked*, complementary to the built-in facility rather than a competitor the language will soon obsolete. + +## The pains CGP removes are real — and developers already hand-roll them + +The most persuasive evidence for a selling point is that developers reinvent CGP's mechanism on their own, and for the central capability they demonstrably do. The pattern CGP is built on — zero-sized marker types plus a helper trait, so several otherwise-overlapping blanket implementations can coexist — has been [independently discovered and blogged](https://www.greyblake.com/blog/alternative-blanket-implementations-for-single-rust-trait/) by a Rust author who reached it to work around the exact "no two blanket impls may overlap" limitation CGP exists to lift, describing the hand-rolled version as "3 extra lines to link things together." This is the single most useful fact in this document for the marketing role: the strongest selling point is not a capability the reader must be talked into wanting, but one they have already built by hand and would rather not maintain. Point to the reinvention as evidence, and the "isn't this over-engineered" reflex softens, because the reader recognizes their own workaround. + +The neighboring pains are evidenced too, and each sharpens a selling point. The dependency-injection frameworks Rust does have are niche and criticized on their own forums — [shaku](https://users.rust-lang.org/t/comparing-dependency-injection-libraries-shaku-nject/102619), for instance, for forcing `Arc` and for being unable to declare multiple implementations per component — which is precisely the limitation CGP's per-context wiring removes, so the honest pitch to that reader is "many implementations per context, no `dyn`," grounded in a complaint they can find themselves. And even hand-rolled trait-based DI leaks: as a [widely-cited post](https://jmmv.dev/2022/04/rust-traits-and-dependency-injection.html) documents, any type named in a public trait's method signature must itself be public, so trait-based injection quietly breaks encapsulation — a concrete cost that CGP's impl-side dependencies avoid, and a selling point [selling-points.md](selling-points.md) now carries. + +## CGP's own reception, and the lessons in it + +CGP's ideas do get discussed, but the reception is niche and runs mixed-to-skeptical, and reading it correctly starts with knowing which venue to trust. The substantive discussion happens on [Lobsters](https://lobste.rs/) and the [Rust subreddit](https://www.reddit.com/r/rust/), not on Hacker News: an HN submission is hit-or-miss, drawing almost no engagement unless it reaches the front page, so a low HN score reflects a submission's title and timing far more than the idea's reception and must not be reasoned from. On Lobsters, where the vote counts are small but the discussion is real, the original [Context-Generic Programming thread](https://lobste.rs/s/a5wfid/context_generic_programming) drew nineteen comments and the [v0.7.0 implicit-arguments thread](https://lobste.rs/s/6gcdzl/supercharge_rust_functions_with) twelve — enough to read what the skepticism actually clusters on, which is where the lessons are. + +Five patterns recur in that discussion, and each maps to guidance elsewhere in the section. + +- **Verbosity and "what problem justifies this."** The most common reaction is that CGP looks like "a very verbose boilerplate," with even an experienced Rust programmer reporting they struggled to follow the samples and asking what concrete problem warrants the complexity. This is the over-engineering reflex from [skepticism.md](skepticism.md) firing in the wild, and it confirms the prescription: lead with a concrete pain and a before/after, never with the paradigm. +- **Traceability of control flow.** A distinct, repeated objection is that the indirection makes a codebase hard to navigate, because it is "basically impossible to tell which particular bit of code will be entered on a method call." This is not the generic "macros are magic" complaint but a specific worry about following execution, now recorded as its own entry in [skepticism.md](skepticism.md); the answer is that the wiring table is the one explicit, greppable place naming the provider for each component. +- **"Isn't this just X reinvented."** Knowledgeable readers reach for prior art — aspect-oriented programming, Microsoft's COM, the ML module system (one comment invoked Greenspun's tenth rule) — as a skeptical frame. These deserve the honest engagement the [related-work](../related-work/README.md) documents supply, not deflection, because the reader making the comparison is exactly the one who can be won or lost on it. +- **The name does not communicate.** Multiple commenters said plainly that "context-generic programming" obscures more than it conveys, that "coining a new phrase makes it harder to understand," and reached instead for "structural typing" or "duck typing for statically-typed code" to name what they thought was being described. This is direct field validation of the [tag-lines.md](tag-lines.md) rule that a name is not a pitch, and it surfaces a community-supplied bridge term worth testing — with the caveat that CGP is nominal-and-wired, not truly structural. +- **Do not overstate the ergonomics.** When CGP claims a reader need not understand its internals, a skeptic answers that "when I hit a compilation error, I'm going to have to understand the desugaring," and they are right. The honest position, per [technical-barriers.md](technical-barriers.md) and [skepticism.md](skepticism.md), concedes the error-message barrier rather than promising the internals stay out of sight. + +Two further lessons come from outside that discussion and still hold. "Dependency injection" is not a safe general hook, because idiomatic Rust already does lightweight DI with traits and generics and a large part of the audience treats DI *frameworks* as an unwanted import — the distinct native objection now in [skepticism.md](skepticism.md). And social proof is worth building: the comparable success stories in adjacent ecosystems turned on a flagship adopter, the way TypeScript's adoption accelerated once a major framework endorsed it, so CGP's most convincing answer to the evaluator's "is anyone really using this" is a real, non-trivial system built with it and shown as a worked example, not more argument. + +## Where the profiles gather + +Attention is channel-specific, so the hook should be chosen for where a piece will actually appear, matching the [reader profiles](reader-profiles.md) to the room. The pragmatic majority and the first-contact skimmer dominate the general channels, and for CGP the discussion has actually landed on [Lobsters](https://lobste.rs/) and the [Rust subreddit](https://www.reddit.com/r/rust/), with the weekly *This Week in Rust* as steady distribution; Hacker News is worth submitting to but hit-or-miss, and matters only when a post reaches the front page, so it should never be a plan's linchpin. In those channels a front-page tag line and a concrete before/after must do the work, and the verbosity, traceability, and "just macros" framings must be preempted in the opening lines. The type-system and functional-programming audience gathers in more specialized corners, where the audience-tuned one-liners in [selling-points.md](selling-points.md) — type classes without the orphan rule, functors with the plumbing automated — land without translation. The evaluator reads long-form: a design document, a detailed write-up, a comparison table, where candor about maturity and cost is what persuades. Pick the channel, then the profile, then the hook — in that order. + +## Reading the reaction + +Because attention is empirical, the real grade of any hook is the reaction it draws, so the section should treat publication as a measurement rather than a conclusion. A hook is working when it draws questions about *how* CGP achieves something; it is failing when the top replies are the dismissals this audience actually reaches for — the ones observed in CGP's own threads above: "verbose / over-engineered," "I can't tell what code runs," "isn't this just AOP / COM / ML modules reinvented," and "the name tells me nothing," alongside the imported "just macros" and "another DI framework." Those replies are not noise but signal that the framing let the reflex fire before the novel part landed, and the fix is upstream, in the opening lines, per [reader-profiles.md](reader-profiles.md) and [tag-lines.md](tag-lines.md). Float a candidate hook where the target profile actually gathers, watch which dismissal it attracts, and revise toward the framing that draws the *how* question instead — the tag-line shortlist in [tag-lines.md](tag-lines.md) is offered for exactly this validation. diff --git a/docs/communication-strategy/formats.md b/docs/communication-strategy/formats.md new file mode 100644 index 00000000..09b92cad --- /dev/null +++ b/docs/communication-strategy/formats.md @@ -0,0 +1,46 @@ +# Formats and channels + +This document turns the audience knowledge in the rest of the section into per-artifact playbooks: for each kind of public writing — a launch post, a tutorial, a README, a talk, a thread, a comparison — the opening move to make, the length to hold to, the dismissal to preempt first, and the action to ask for. + +## How to use this document + +A piece of CGP writing is shaped by its format as much as by its message, so the first decisions are made in a fixed order: pick the channel and the reader before the words. [attention-and-engagement.md](attention-and-engagement.md) says where the audience actually gathers and what earns its attention; [reader-profiles.md](reader-profiles.md) says who is reading; this document says how to structure the artifact once those are chosen. The order is channel → profile → format → hook, and skipping it is how a piece ends up written for no one in a form its channel punishes. + +Every playbook below shares four moving parts, and naming them once keeps each entry short. The **opening move** is the first thing the reader meets and the only thing a skimmer may read, so it carries a concrete pain or a runnable example, never the paradigm. The **length and depth** are set by the channel's patience and the reader's place on the [prerequisite ladder](technical-barriers.md). The **dismissal to preempt** is the reflex that format's audience fires first — "verbose / over-engineered," "just macros," "another DI framework," "the name says nothing" — which must be defused in the opening lines, because it is answered upstream or not at all. And the **call to action** is the single next step the piece asks for, matched to what that reader is ready to do; the conversion ladder at the end sets those out. These moving parts and the other terms of the craft that recur below — hook, tag line, above the fold — are defined in the [glossary](glossary.md) for a reader meeting them for the first time. + +## The link-aggregator launch post (Lobsters and r/rust) + +This is where CGP is actually discussed, so it is the format to get right first, and its reader is the pragmatic skimmer who will form and broadcast a snap judgment. Title it with a concrete capability, not the paradigm name — "reusable, swappable trait implementations for Rust" over "context-generic programming" — because the title is the whole pitch for most of the audience, per [tag-lines.md](tag-lines.md). Open the body on a runnable `Hello World` or a five-line before/after from [problems-solved.md](problems-solved.md); the one piece of feedback CGP's own launch received asked for exactly this ([attention-and-engagement.md](attention-and-engagement.md)), and it is what the format rewards. Keep it short and let the linked material carry the depth. Preempt the two dismissals this audience fires fastest — "verbose / over-engineered" and "why not just traits" — in the first paragraph, by naming the concrete pain that justifies the machinery and conceding where a plain trait would suffice, per [positioning.md](positioning.md). Then be present in the thread: this channel expects the author to answer, and the canned responses below are what to keep ready. The call to action is the quickstart, not the full paradigm. + +## The blog tutorial or deep-dive + +A tutorial has the reader's sustained attention but must earn each step, so it lives or dies on progressive disclosure. Open on a concrete problem the reader already has — a mock swapped for a real implementation, a trait grown monolithic — never on the consumer/provider split or coherence, per [technical-barriers.md](technical-barriers.md). Lead with the vanilla-looking idioms ([`#[cgp_fn]`](../reference/macros/cgp_fn.md), [`#[cgp_impl]`](../reference/macros/cgp_impl.md), [`#[implicit]`](../reference/attributes/implicit.md)) so early code reads like ordinary Rust, and reveal the machinery — the [`DelegateComponent`](../reference/traits/delegate_component.md) table, the desugaring — only when a reader has a reason to want it. Carry one running example the whole way through rather than switching examples per concept, so understanding compounds. Set the error-message expectation honestly before the reader hits one: CGP's diagnostics can be verbose, and a [`check_components!`](../reference/macros/check_components.md) localizes them. The call to action is the next tutorial or the [CGP Patterns book](https://patterns.contextgeneric.dev/), the depth a motivated reader graduates into. + +## The README and landing page + +The README's job is the first screen, where an evaluator and a skimmer both decide in seconds whether to continue. Put the layered tag line at the top — the owned name paired with a plain descriptor and a subline that heads off the runtime-container and new-language misreadings, per [tag-lines.md](tag-lines.md) — then the small headline set from [key-features.md](key-features.md), then a runnable `Hello World` above the fold (the first screen, before the reader scrolls), because a reader wants to see the code before the prose. Keep the feature set to the ruthless few; a list of ten reads as a list of none. State "a library on stable Rust" and the install line early, since the [evaluator](reader-profiles.md) is scanning for the toolchain gamble. The call to action is the quickstart for the skimmer and a link to the honest maturity-and-adoption discussion for the evaluator. + +## The conference talk or video + +A talk is the format with the most room to motivate, and its audience forgives depth if the *why* comes first. Open on the pain and the before/after, spend the middle on the single "aha" — that the same call resolves to a different implementation per context, chosen in one readable line and compiled to a direct call — and defer the coherence theory to a late "how it works" section for the audience that stayed for it. Show the vanilla-looking code on the first slides and the machinery only after the value has landed. Close on honest limits and where *not* to reach for CGP, because a talk that concedes its boundaries is the one an evaluator trusts. The call to action is the quickstart and the repository. + +## The social thread + +A thread on Mastodon, Bluesky, or X has seconds of attention and one job: earn the click without inviting the dunk. Lead with one concrete pain or one striking before/after image, keep jargon out of the first post entirely, and let a single link carry the depth. Do not open on the paradigm name, and do not compress the pitch so far that it reads as an overclaim — the skimmer who feels oversold broadcasts that reaction. This format is a hook delivery mechanism, not a place to explain; the call to action is a link to the post or quickstart that can. + +## The comparison or positioning piece + +A comparison earns disproportionate trust when it is honest and disproportionate damage when it is not, so its rule is the section's non-negotiable one: never disparage the compared tool, and name where it is simply the better choice. Represent every alternative as its own users would recognize it, drawing the sentiment and the mapping from the [related-work](../related-work/README.md) documents, and route the "when to use which" judgment through [positioning.md](positioning.md). A comparison table is a strong form here, provided each row concedes the case where the other tool wins; a table that shows CGP winning every row reads as a strawman and loses the reader it most wanted. The call to action is the honest decision guide, not a verdict. + +## Answering in threads + +Replying in a discussion is its own format, and because the general channels expect the author present, a small set of ready answers is worth keeping — each conceding the real cost before making the point, in the wording [skepticism.md](skepticism.md) fixes. The recurring questions and the shape of their answers: + +- **"Isn't this just a DI framework / macros / over-engineering?"** Concede that Rust needs no DI *framework* and that CGP is not one, then locate the concrete case that plain traits handle awkwardly — many implementations per context, an orphan-rule escape, a leaked-internals fix — per [positioning.md](positioning.md). +- **"How do I tell what code actually runs?"** Concede the indirection, then point at the wiring table as the one greppable place that names the provider for each component, per [skepticism.md](skepticism.md). +- **"What about compile times / the error messages?"** Concede both directions honestly, decline to invent a number, and name [`check_components!`](../reference/macros/check_components.md) and the compile-time-versus-runtime trade as the real mitigations. +- **"Why not just use traits?"** Agree that for one implementation, or a closed set, the plain tool wins, and point at the [modularity hierarchy](../concepts/modularity-hierarchy.md) as the map of when to climb. + +## The conversion ladder + +Every format ends by asking for a next step, and the right step depends on the reader, so the call to action is chosen from the profile, not fixed. The **first-contact skimmer** is ready only for a low-commitment look — the quickstart, a runnable example, the repository — so ask for that and nothing heavier. The **working developer** is ready to try CGP on a real problem, so point them at the [problems-solved](problems-solved.md) narrative closest to their pain and the guide that walks it. The **evaluator** is deciding on risk, so offer the honest maturity discussion, the incremental-adoption story, and a real system built with CGP as social proof — the flagship-adopter evidence [attention-and-engagement.md](attention-and-engagement.md) argues is worth building. And the **enthusiast** is ready to go deep, so hand them the [CGP Patterns book](https://patterns.contextgeneric.dev/) and the contribution path. Asking a reader for more than their rung is ready for is how a piece that earned attention loses the conversion. diff --git a/docs/communication-strategy/glossary.md b/docs/communication-strategy/glossary.md new file mode 100644 index 00000000..faf80121 --- /dev/null +++ b/docs/communication-strategy/glossary.md @@ -0,0 +1,51 @@ +# Glossary of the non-technical craft + +This document defines, in plain language, the marketing, public-communication, and developer-relations terms of art that the rest of the section uses — each anchored to an intuition a systems programmer already holds — so a reader fluent in CGP but new to these disciplines never has to guess what a word means. + +## How to use this glossary + +This is the quick-reference companion to the principles in the [README](README.md), not a replacement for them. The README explains the *ideas* — positioning, framing, the funnel — as an argument you read straight through; this document is the *lookup* you scan when a term appears and you want its one-line meaning and a programmer's analogy for it. Where a term has a fuller treatment in the README's principles section or in a dedicated document, the entry here gives the short gloss and points you there for the depth. + +A word on why the analogies matter. The fastest way to learn a concept from a discipline you have never studied is to map it onto one you already own, so every entry ties its term to something a systems programmer knows cold — a function signature, a cache miss, a fast path, an API boundary. Treat the analogy as a handhold rather than an equation: it is close enough to make the term stick, and the document it points to carries the precise version. None of this is difficult or secret, as the README stresses; it is a separate discipline with its own words, and this is the word list. + +## The parts of a piece + +Every piece of public writing is built from a small set of named parts, and naming them once is what lets the per-artifact playbooks in [formats.md](formats.md) stay short. These are the anatomy of a landing page, a launch post, or a talk. + +- **Hook** — the opening line whose only job is to make the reader read the next line. It is the fast path of a piece: nearly every reader hits it, most go no further, so if it is slow or unclear nothing downstream ever runs. A good hook carries a concrete benefit or a striking before/after, never the paradigm name. +- **Tag line** — the compressed, one-line description of the whole project, usually the first thing a reader learns about it and sometimes the only thing. It is the project's `--help` summary line: a handful of words that must say what the thing is and why to care. The full treatment, with CGP's candidates weighed, is in [tag-lines.md](tag-lines.md). +- **Subline** — the second line beneath a hook or tag line that carries the honest mechanism the hook left out. The hook earns the click; the subline heads off the reader's first objection — "still Rust, checked at compile time, no runtime cost" — before it can form. +- **Call to action (CTA)** — the single next step the piece asks the reader to take, such as "try the quickstart" or "read the comparison." Offer exactly one, the way a good CLI has one obvious default action: a reader handed five next steps takes none. Matching the CTA to the reader's stage is the job of the conversion ladder in [formats.md](formats.md). +- **Above the fold** — what a reader sees before scrolling, a phrase borrowed from the top half of a folded newspaper's front page. On a README or landing page it is the first screen, and it must carry the tag line, the headline features, and ideally runnable code, because many readers judge from it alone and never scroll. + +## Getting attention and choosing the category + +Marketing supplies the words for how an idea earns a reader's attention and which category the reader files it under, developed in full in the README's [marketing principles](README.md#marketing-positioning-and-attention). These terms recur wherever the section discusses what to lead with. + +- **Positioning** — the deliberate choice of the category a reader files you under, made before they choose a dismissive one for you ("oh, it's a DI framework"). A reader pattern-matches a new tool to the nearest thing they know within seconds, so positioning is picking that match for them rather than leaving it to chance. It is the subject of [positioning.md](positioning.md). +- **Framing** — the choice of angle and wording that decides how a reader reacts to a fact that is true either way. "Overlapping instances made safe" and "clever type-system trickery" can describe the same feature; the frame is the message, not a decoration on it. +- **Value proposition** — the one-sentence answer to "what do I get, and why should I care?", stated as a benefit rather than a feature list. It is the return value of your pitch: if a reader cannot state it after your opening, the call returned nothing. +- **Differentiation** — what CGP does that the obvious alternative cannot, the answer to "why this and not the thing I already use." A reader who sees no difference keeps what they have, so a pitch must name the gap out loud. +- **Elevator pitch** — the thirty-second spoken explanation you could finish before an elevator reaches its floor, the conversational cousin of the tag line. Its constraint is the same: one idea, delivered before attention runs out. + +## Reaching people at scale, and sounding like one project + +Public communication is the craft of being understood by readers you will never meet, and its terms are about clarity and consistency across a whole body of writing rather than any single piece. The README's [public-communication principles](README.md#public-communication-clarity-and-consistency) argue why these invert the habits technical writing trains. + +- **Channel** — where a piece appears: Lobsters, the Rust subreddit, a conference talk, a social thread, the README. Each channel has its own audience, patience, and etiquette, so the channel is chosen before the words are; [attention-and-engagement.md](attention-and-engagement.md) records where CGP's readers actually gather. +- **Curse of knowledge** — the expert's built-in inability to feel what a beginner does not yet know, which makes deep CGP fluency the single biggest risk to explaining CGP well. It is the reason a compiler author writes the error message only they can read. It is the master principle behind nearly every warning in the section, stated in the [README](README.md#principles-from-marketing-public-communication-and-developer-relations) and answered construct by construct in [technical-barriers.md](technical-barriers.md). +- **Message discipline** (or **one voice**) — using the same word for the same idea in every piece, so scattered posts reinforce one another instead of reading as different projects. It is API stability for prose: rename the concept per post and the reader senses incoherence even when they cannot name it. The canonical word list is [vocabulary.md](vocabulary.md). +- **Clarity over completeness** — the rule that one idea a reader keeps beats five accurate ideas they forget, so a pitch says less, more clearly, and defers the nuance. It fights the specification writer's instinct, where completeness is the virtue; in a pitch, completeness is what buries the point. + +## Earning and keeping trust + +Developer relations is communication with an audience that has been marketed to badly its whole career and detects spin on sight, so its terms are about trust — how it is earned, and how one misstep spends it. The README's [developer-relations principles](README.md#developer-relations-trust-and-community) explain why honesty is the strategy rather than a limit on it. + +- **Social proof** — evidence from other people, especially peers and real running systems, that persuades where self-praise cannot. A developer believes another developer's working code far more than any adjective you write; the section argues a flagship adopter is CGP's most valuable missing asset, in [attention-and-engagement.md](attention-and-engagement.md). +- **The funnel** (and **conversion**) — the staged path a reader travels from first hearing of CGP to advocating for it: heard-of, curious, trying, adopting, advocating. It is called a funnel because readers drop out at every stage, so fewer reach each next one, and a **conversion** is one reader taking the step to the next stage. Each stage wants different content and a different [call to action](#the-parts-of-a-piece) — the **conversion ladder** the [formats.md](formats.md) playbooks end on. +- **Show, don't tell** — demonstrate a capability with running code rather than assert it with an adjective, because this audience trusts what it can run and discounts what it is merely told. A before/after on real code outperforms every "flexible" and "powerful" combined. +- **The pile-on** (and **dunking**) — a public group dismissal, where a technical community turns on a post it reads as arrogant or dishonest and piles ridicule on it, often outliving the post. "Dunking" is the act of publicly ridiculing a claim, and a pile-on is the crowd version. The defense is the same as the honest move — claim only what is true, and never disparage another tool to elevate CGP, per the [README](README.md#developer-relations-trust-and-community). + +## When a term is missing + +If you meet a non-technical term in the section that is not defined here, treat that as a gap to close rather than a word to guess at. Add the term to this glossary in the same change and in the same shape — a plain definition and a programmer's anchor — so the next reader is not left guessing, and so this document stays the section's single home for the vocabulary of the craft, the way [attention-and-engagement.md](attention-and-engagement.md) is the single home for its external citations. diff --git a/docs/communication-strategy/key-features.md b/docs/communication-strategy/key-features.md new file mode 100644 index 00000000..8021efbf --- /dev/null +++ b/docs/communication-strategy/key-features.md @@ -0,0 +1,27 @@ +# Key features + +This document sets out the key features CGP should put at the front of its website and README — the few best selling points, distilled to a title and a sentence each. + +## What a key feature is, and how it differs from a selling point + +A key feature is a headline: a title of a few words and a sentence beneath it, scannable in the seconds a visitor gives a landing page before deciding whether to read on. It is not the same artifact as a selling point. [selling-points.md](selling-points.md) is the *full catalog* — every true, advertisable capability, each with the audiences it serves and the phrasings that land — and it is deliberately comprehensive because a writer picks from it per piece and per reader. The key-features list is the opposite: a small, fixed set shown to everyone at once, where the constraint is not coverage but ruthlessness. Only the best few selling points earn a place, because a feature list is read as a whole and its power is inversely proportional to its length. + +Three rules follow from that, and they are the criteria this document applies. **Short**: each feature is a title and one or two sentences, never a paragraph — the elaboration lives in the longer "problems solved" material and the pattern book, not in the headline. **Few**: four to six features, because a list of ten reads as a list of none, and a reader remembers three. **The best only**: each feature must be one of the strongest, broadest, most honest selling points, chosen with the [reader profiles](reader-profiles.md) in mind and worded by the lessons in [tag-lines.md](tag-lines.md) and [skepticism.md](skepticism.md) — which means, above all, that a feature *title* must not lead with a word that repels, and a feature *sentence* must not overclaim to the audience most able to check it. + +## The key features + +The five features below are the headline set, ordered most-important first, each with the title, the sentence to ship, and the reason it earns a slot. Each is chosen to be broad, honest, and free of the words that trigger the reflexes catalogued in [skepticism.md](skepticism.md). + +- **One Interface, Many Implementations** — *"Write many interchangeable implementations of the same interface and choose between them per context, with overlapping and orphan implementations that Rust normally forbids made safe because every choice is explicit and local."* This is CGP's core identity: it names the capability directly and grounds the coherence-freedom advantage (see [bypassing coherence](../concepts/coherence.md)) that the type-system audience prizes. +- **Zero-Cost Abstraction** — *"Everything is resolved at compile time and compiles down to direct calls, so the flexibility costs nothing at runtime and unused providers never reach the binary."* This is the strongest broad reassurance for the Rust audience and the property that sets CGP apart from the runtime tools other languages use; it uses a recognized Rust term in the title while stating the concrete mechanism in the sentence. +- **Type-Safe Wiring** — *"All wiring is checked at compile time, so a missing dependency is a build error rather than a runtime failure — and it runs entirely in safe Rust, with no `dyn`, `Any`, or reflection."* This is the differentiator against both dependency-injection frameworks and dynamic dispatch, and it carries the [`check_components!`](../reference/macros/check_components.md) guarantee that no CGP-specific error can reach runtime. +- **Abstract Over Every Dependency** — *"Write core logic that names its error type, runtime, and I/O abstractly and let each context supply the concrete choice — which keeps the core `no_std`-friendly, from embedded systems and kernels to WebAssembly."* This speaks directly to the systems programmer: it makes the error type, the runtime, and I/O swappable, and its payoff is a core that runs anywhere, down to the most constrained targets. +- **Still Ordinary Rust** — *"CGP is a superset of ordinary traits that you adopt one piece at a time: providers read like normal impls and implicit arguments like normal parameters, so a codebase can use it in one corner and stay otherwise vanilla."* This earns its slot by defusing the biggest adoption fear — that CGP is an all-or-nothing new paradigm — which no other feature addresses and which the [reader profiles](reader-profiles.md) show is the decisive worry for the pragmatist and the evaluator. + +## What the headline set deliberately leaves out + +Two absences are deliberate and worth naming, because each is a tempting addition that would weaken the set. First, there is no feature titled **"Dependency Injection,"** even though CGP is, in substance, compile-time dependency injection: on a general front page the label imports the runtime-framework and "magic" baggage documented in [skepticism.md](skepticism.md) and the [dependency injection](../related-work/dependency-injection.md) comparison, so the DI value is delivered through "Type-Safe Wiring" and "Abstract Over Every Dependency," and the explicit "dependency injection, without the framework" pitch is reserved for the audience-targeted channels where it lands, per [selling-points.md](selling-points.md). Second, CGP's reach into specific domains — **error handling, async runtimes, alternatives to dynamic dispatch, and decomposing large traits** — stays out of the headline set, because those are elaborations of the five features above rather than peers of them; they belong in the longer-form material a reader reaches after the headlines have earned their attention. + +## Phrasing principles for feature titles + +Feature titles obey the same wording discipline as tag lines, compressed to a few rules. Lead a title with the **concrete capability, not an abstraction or a mechanism** — "One Interface, Many Implementations" rather than a title built on "modularity" or "macros." Avoid the words that trigger the documented reflexes: **"modular," "macros," and "magic"** as lead words each cost more attention than they win. Use the **recognized Rust terms** that aid scanning — "zero-cost," "type-safe," "no-std" — where they are accurate, since their familiarity is an asset in a title. And write each feature *sentence* to name a benefit and, wherever a skeptic would balk, the honest qualifier in the same breath — "at compile time," "in safe Rust," "still ordinary Rust" — because the qualifier is what turns a claim the reader would otherwise discount into one they believe. diff --git a/docs/communication-strategy/positioning.md b/docs/communication-strategy/positioning.md new file mode 100644 index 00000000..0cff315d --- /dev/null +++ b/docs/communication-strategy/positioning.md @@ -0,0 +1,40 @@ +# When to use CGP, and when not + +This document is the honest map of where CGP earns its cost and where a plainer tool wins, so a writer can tell a reader when to reach for it and — just as importantly — when not to, because naming the boundary is what makes the recommendation believable. + +## Why the boundary is a marketing asset + +The instinct to sell a tool as universally better is exactly the instinct this audience punishes, so the developer-relations move is to draw CGP's boundary in public rather than let a skeptic draw it for you. The Rust community's default reaction to a new abstraction is "this is over-engineered," and the fastest way to disarm it is to show the author declining to apply CGP everywhere: a reader who watches you say "for this, use a plain trait" believes you about the cases where you say "here, use CGP." This document supplies those lines. It is the positioning companion to [skepticism.md](skepticism.md), which answers "why not just traits" defensively; here the same judgment is offered proactively, as a decision guide a reader can act on. It is a *public positioning* artifact, not a re-teach of the [modularity hierarchy](../concepts/modularity-hierarchy.md) concept — that document is the technical map, and this one is the shorter, honest table a marketing piece or a thread reply reaches for. + +The register throughout is candor, not enthusiasm, because the reader most likely to consult a positioning guide is the [evaluator](reader-profiles.md), whose job is to discount hype. Concede the range where the plainer tool wins in plain words, then state where CGP starts to pay, and let the concession carry the credibility of the recommendation. + +## The rule of thumb + +The one principle that settles most cases is to reach for the lowest rung that expresses the problem, because each step up trades simplicity for modularity a given problem may not need. A capability with a single universal implementation is a function or a blanket trait; a capability that varies by type but never by application is an ordinary trait or an enum; a capability that needs several interchangeable implementations chosen per context, or an implementation for a type you do not own, or overlapping implementations that coherence forbids, is where CGP earns its cost. Said as one line a writer can reuse: **use CGP when a capability needs more than one implementation and the choice belongs to the context — not before.** The full ladder, rung by rung, is the [modularity hierarchy](../concepts/modularity-hierarchy.md); this rule is its compression. + +## The decision guide + +The recurring question is not "is CGP good" but "CGP or this other thing," so the guide below takes the alternatives a reader will actually weigh and, for each, names when the alternative is the right call and when CGP starts to pay. Each entry concedes the alternative's home ground first, because that concession is the point. + +- **A plain trait or generic.** Prefer it when a capability has one implementation, or one per type and a single global choice per type is fine — this is most code, and CGP would be over-engineering. Reach for CGP when the implementations multiply, when the choice must differ per context (mock versus real, per deployment), or when threading a generic parameter through every layer has begun to hurt. The honest framing, per [skepticism.md](skepticism.md), is that CGP is a *superset* of the trait approach, so this is a climb from the plainer tool, not a rejection of it. +- **An enum.** Prefer it for a small, closed, known set of variants with a fixed set of operations — a match is clearer than any machinery. Reach for CGP's [extensible variants](../concepts/extensible-variants.md) when the variant set is open, when independent modules must each contribute a variant or an operation, or when you want the compile-time exhaustiveness of a match without editing every match as the set grows. +- **`dyn Trait` and runtime dispatch.** Prefer it when the set of implementations is not known until runtime — plugins loaded at startup, a heterogeneous collection, a choice made from configuration read at boot — because that runtime openness is exactly what CGP gives up. Reach for CGP when the set of implementations *is* known at build time and you want the decoupling of dynamic dispatch with none of its cost, per [dynamic dispatch](../related-work/dynamic-dispatch.md). This is a genuine either/or, and a piece should say so. +- **A dependency-injection crate.** Prefer a runtime DI container only when you specifically want its runtime lifecycle and object-graph semantics. Reach for CGP when you want compile-time-checked, reflection-free injection with a missing dependency caught at build time — and note that Rust's DI crates are niche and criticized for exactly the limits CGP lifts, such as being unable to declare multiple implementations per component ([attention-and-engagement.md](attention-and-engagement.md), [dependency injection](../related-work/dependency-injection.md)). The care here is to frame CGP as the traits-and-generics approach the reader already prefers, not as a framework, per [skepticism.md](skepticism.md). +- **A generic-programming toolbelt like `frunk`.** Prefer the lighter library when a one-off `HList` or generic-representation manipulation is all a piece of code needs. Reach for CGP's [extensible data](../concepts/extensible-records.md) when the structural machinery is part of a larger component-and-wiring design rather than a standalone transform. Represent the alternative as its own users would, per the section's no-disparagement rule. +- **A hand-rolled macro.** Prefer a small bespoke macro when the code generation is narrow and self-contained. Reach for CGP when you find yourself reinventing its mechanism — the marker-struct-plus-helper-trait workaround for overlapping blanket impls that developers already write by hand ([attention-and-engagement.md](attention-and-engagement.md)) is CGP's core, and doing it once through CGP beats maintaining it per project. +- **Waiting for a language feature.** Prefer waiting when a first-class facility would serve better and you can afford to — Rust's [effects initiative](../related-work/algebraic-effects.md) and its [reflection-and-comptime work](../related-work/reflection.md) are pursuing built-in versions of ground CGP covers. Reach for CGP when you need the capability now, on stable Rust, as a library rather than a nightly feature or an unmerged proposal. The honest note is that CGP is available today and complementary to what the language is building, not a bet against it. + +## Where CGP is simply not the answer + +A few cases are not trade-offs but clear misfits, and saying so plainly is the most trust-building move in any positioning piece. CGP resolves entirely at compile time, so it cannot provide **runtime dynamism** — plugins loaded at startup, monkey-patching, heterogeneous collections, a graph mutated while the program runs — and for those Rust's own `dyn Trait` and the dynamic languages remain the right tools ([dynamic dispatch](../related-work/dynamic-dispatch.md)). A capability with **exactly one implementation** belongs in a plain trait or function; a **small closed variant set** belongs in an enum. And when a program genuinely wants **one instance program-wide** — a single, globally consistent `Ord` for a map key, say — CGP's deliberate per-context choice is the wrong shape, and coherent type classes or a plain trait, which guarantee that global uniqueness, are the better and safer tool ([type classes](../related-work/type-classes.md), [bypassing coherence](../concepts/coherence.md)). Naming this last case is especially disarming to the advanced reader who raised the "coherence exists for a reason" objection, because it shows CGP knows the limit of its own bargain. + +## How to say it + +The wording that makes a positioning claim land follows the section's honesty discipline: concede the alternative's range before asserting CGP's, and prefer a boundary to a superlative. Say it like this: + +- "For one implementation, use a trait. CGP earns its keep when you need several, chosen per context." +- "If your set of implementations is known at compile time, CGP gives you the decoupling of `dyn` with none of the cost. If it isn't, use `dyn`." +- "Rust doesn't need a DI framework, and CGP isn't one — it's the traits-and-generics approach you already use, carried to the cases where doing it by hand stops scaling." +- "Reach for the lowest rung that solves your problem; CGP is a rung you climb to deliberately, not a default." + +Avoid the framings that overreach: do not call CGP "better than traits" (it is a superset you climb to), do not imply it replaces `dyn` (it trades away runtime openness), and do not sell it into a problem with one implementation (that is the over-engineering the reader fears). The concession is not a weakness in the pitch; with this audience it is the pitch, and [skepticism.md](skepticism.md) and [problems-solved.md](problems-solved.md) are where the same boundary is drawn from the objection and the pain sides. diff --git a/docs/communication-strategy/problems-solved.md b/docs/communication-strategy/problems-solved.md new file mode 100644 index 00000000..a997a979 --- /dev/null +++ b/docs/communication-strategy/problems-solved.md @@ -0,0 +1,103 @@ +# Problems solved + +This document catalogs the concrete problems CGP removes, written as short before-and-after stories a writer can lead with, so that any piece opens on a pain the reader already feels rather than on the paradigm. + +## How to use these problems + +The whole strategy is problem-first, and this is where the problems live: each entry is a familiar Rust pain, the awkward workaround a developer reaches for today, and the CGP version that removes it, sized down to the smallest honest before/after. This document is the third axis of the section, alongside the capabilities in [selling-points.md](selling-points.md) and the objections in [skepticism.md](skepticism.md) — a selling point says what CGP *can do*, an objection says what a reader *fears*, and a problem says what *hurts now*, which is the one a skeptic will actually follow. Reach for the problem that matches the reader before reaching for any selling point, because "here is the thing you fight every week, gone" persuades where "look what CGP can do" does not. + +Three rules govern how to use an entry. Lead with the **pain, not the mechanism** — open on the workaround the reader recognizes, and let the CGP version arrive as relief rather than as a new thing to learn. Keep the **before/after small and honest** — a five-line diff on ordinary-looking code outperforms a full example, and the "before" must be code the reader would genuinely write, not a strawman that makes CGP look better than it is. And **name the limit in the same breath**, because every entry below is also a place a reader could over-apply CGP, and the honest boundary is what makes the win believable; when the plainer tool is the right one, [positioning.md](positioning.md) says so. + +Each problem names the reader profile it lands with, the selling point and skepticism it maps to, and the honest cost, so a writer can move from a problem straight to the wording that sells it and the objection to defuse. The code shown uses the modern idioms the `/cgp` skill teaches — a provider written with [`#[cgp_impl]`](../reference/macros/cgp_impl.md), values read with [`#[implicit]`](../reference/attributes/implicit.md), wiring with [`delegate_components!`](../reference/macros/delegate_components.md) — because a "before/after" that showed dated CGP would teach the reader a dialect they must later unlearn. + +## Mock in tests, run the real thing in production — without `dyn` or a framework + +The most universal pain CGP removes is swapping a real implementation for a fake one across tests and production, which every Rust developer has solved awkwardly. The usual routes each cost something: a `Box` field pays runtime dispatch and infects the type with a trait object; a generic `` parameter threads through every layer that touches it and multiplies as more dependencies join; and a dependency-injection crate brings machinery the Rust community broadly distrusts, as [attention-and-engagement.md](attention-and-engagement.md) records. CGP makes the swap a single wiring line, monomorphized to a direct call. + +The capability becomes a component, and each environment wires the provider it wants: + +```rust +#[cgp_component(EmailSender)] +pub trait CanSendEmail { + fn send_email(&self, to: &str, body: &str); +} + +#[cgp_impl(new SendViaSmtp)] +impl EmailSender { /* connect and send over SMTP */ } + +#[cgp_impl(new RecordEmails)] +impl EmailSender { /* push to a Vec so a test can assert on it */ } + +delegate_components! { App { EmailSenderComponent: SendViaSmtp } } +delegate_components! { TestApp { EmailSenderComponent: RecordEmails } } +``` + +`App` sends real mail and `TestApp` records it; neither pays for `dyn`, the swap is one greppable line, and code that calls `self.send_email(..)` never changes. This is the entry to lead with for the **working developer** ([reader-profiles.md](reader-profiles.md)), it maps to the [swap-implementations selling point](selling-points.md), and it defuses the ["why not just use traits"](skepticism.md) reflex by showing the case where a plain generic would have proliferated. The honest limit: the choice is a line *you* write, not one CGP infers, and for a dependency with exactly one implementation a plain trait is still the right tool. + +## Implement a trait for a type you don't own — no newtype dance + +A pain sharp enough that Rust developers hand-roll CGP's own mechanism to escape it is the orphan rule: you cannot implement a foreign trait for a foreign type, so adding behavior to a type from another crate means wrapping it in a newtype and re-exposing every method you still need — tedious, and it clutters the code with a wrapper the rest of the program must thread around. This is not a theoretical annoyance: there is a repository cataloguing the rule's design problems and a steady stream of posts on the workaround, and one Rust author [independently reinvented CGP's exact marker-struct pattern](attention-and-engagement.md) to get around it, calling the hand-rolled version "3 extra lines to link things together." + +CGP dissolves the orphan rule because a provider implements the *provider* trait for its own zero-sized marker, not the target trait for the foreign type, so coherence never bites — a crate can add a capability to a type it does not own with no wrapper. The mechanics are the subject of [bypassing coherence](../concepts/coherence.md), and the pitch's power is recognition: the reader has written the three-line workaround and would rather not maintain it. This lands with the **working and advanced developer** ([reader-profiles.md](reader-profiles.md)), maps to the [overlapping-and-orphan selling point](selling-points.md), and answers the ["coherence exists for a reason"](skepticism.md) objection by locating CGP's discipline. The honest limit: CGP does not repeal coherence globally; it keeps each choice explicit and per-context, and where a program genuinely wants one instance program-wide, a coherent trait is the better fit. + +## Give one interface many implementations that would otherwise collide + +Closely related but distinct is wanting *several* implementations of one capability at once — the case Rust forbids outright, because two blanket impls that could overlap are rejected even in theory. A developer who wants to serialize a value three ways, or handle an error type by several strategies, hits a wall the language will not let them past without the marker-struct contortion. CGP is built for exactly this: overlapping providers coexist because each is a distinct marker type, and a context selects one explicitly, so the choice is unambiguous locally without global uniqueness. + +The clearest illustration is per-value dispatch, where two applications encode the same type differently, each coherent within itself: + +```rust +delegate_components! { + AppA { + open ValueSerializerComponent; + @ValueSerializerComponent.Vec: SerializeHex, + } +} + +delegate_components! { + AppB { + open ValueSerializerComponent; + @ValueSerializerComponent.Vec: SerializeBase64, + } +} +``` + +`AppA` serializes bytes as hex and `AppB` as base64, and the overlapping providers never conflict because each context names one. This is the standout for the **type-system and functional-programming reader** ([reader-profiles.md](reader-profiles.md)), it maps to the [overlapping-and-orphan selling point](selling-points.md) and its "type classes without the orphan rule" one-liner, and it is the case [type classes](../related-work/type-classes.md) frames in full. The honest limit: this is genuinely more machinery than a single trait needs, so it earns its place only when the several implementations are real — the boundary [positioning.md](positioning.md) draws. + +## Swap your error type or runtime by changing one line + +A quieter pain, felt hardest in libraries and reusable cores, is that fallible code commits to a concrete error type early and then cannot easily change it, so a decision to move from `anyhow::Error` to a custom enum, or from one async runtime to another, ripples through every signature. CGP makes the error type — or the runtime, or any cross-cutting type — abstract, chosen by the same wiring that selects behavior, so the concrete choice lives in one place and generic code never names it. + +Code written against an abstract [`HasErrorType`](../reference/components/has_error_type.md) works unchanged whichever error a context wires: + +```rust +delegate_components! { + App { + ErrorTypeProviderComponent: UseType, + } +} +``` + +Switching that line to a custom `AppError` retargets every fallible provider in the context, touching no logic. This resonates with the **ML-module and systems reader** ([reader-profiles.md](reader-profiles.md)) and underpins CGP's [modular error handling](../concepts/modular-error-handling.md); it maps to the [abstract-types selling point](selling-points.md) and attaches to the live `anyhow`-versus-`thiserror` conversation ([attention-and-engagement.md](attention-and-engagement.md)). The honest limit: CGP defers and configures the type, it does not seal a representation the way an ML module does — hiding a representation is still Rust's module privacy, and the wiring keys live under `cgp::core::error`, not the prelude, so a piece that shows this should import them. + +## Break up a trait that grew into a monolith + +A pain that arrives with a codebase's age rather than its domain is the trait that accreted responsibilities until every implementor must supply the whole surface and every change touches all of them — the "god trait" that resists decomposition because splitting it breaks impls and threads new generic parameters everywhere. CGP lets one large capability become a set of independently wired components, each with its own providers, so an implementor supplies only what it uses and a context assembles the pieces without a single monolithic impl. + +Because each component is wired separately, a context can compose a behavior from many small providers and reuse each across contexts, and adding a capability is adding a wiring line rather than editing a shared trait every type implements. This is the decoupling pitch for the **framework author and the evaluator weighing maintainability** ([reader-profiles.md](reader-profiles.md)); it maps to the [gradual-adoption selling point](selling-points.md) and the decoupling benefits, and it rests on the [consumer/provider split](../concepts/consumer-and-provider-traits.md). The honest limit: decomposition is a judgment call, and a small, stable trait with one implementation should stay a plain trait — the [modularity hierarchy](../concepts/modularity-hierarchy.md) is the map of when the split pays, and [positioning.md](positioning.md) draws the line for a marketing piece. + +## Write a framework over any type's structure — without runtime reflection + +For the author of a serialization library, a builder, an ORM, or a configuration system, the recurring pain is code that must work over the *shape* of a user's type without a general reflection facility Rust lacks, which pushes them toward per-type derive macros, stringly-typed field access, or a reflection crate that walks a descriptor at runtime. CGP encodes a type's fields and variants as type-level data the trait system resolves against, so a framework written once recurses over any type that opts in with a derive, fully checked when written and with nothing walked on each call. + +A type opts in with a derive, its structure becomes a type-level list, and generic code processes it with static checking and no runtime introspection — the payoff of reflection without its cost, developed in [extensible records](../concepts/extensible-records.md) and [extensible variants](../concepts/extensible-variants.md). This is the entry for the **framework and tooling author** ([reader-profiles.md](reader-profiles.md)), it maps to the [generic-over-structure selling point](selling-points.md), and it sits in the live reflection-and-comptime conversation ([attention-and-engagement.md](attention-and-engagement.md), [reflection](../related-work/reflection.md)). The honest limit, stated in the same breath: it works only on types that derive the shape and does no runtime introspection, so a reader must not be sold "reflection for Rust" and then look for an API to query a type — the precise frame is "compile-time structural reflection encoded in types." + +## Keep a provider's dependencies out of your public API + +A subtler pain, felt by library authors, is that trait-based dependency injection leaks: because any type named in a public trait's method signature must itself be public, exposing a dependency through a generic trait forces internal types into the public API and breaks encapsulation, a cost [documented with its source](attention-and-engagement.md) in the Rust community. CGP keeps a provider's dependencies as impl-side constraints — declared where the implementation lives, through [`#[uses]`](../reference/attributes/uses.md) and [`#[implicit]`](../reference/attributes/implicit.md), rather than in the consumer trait's signature — so the public interface stays clean while the requirements are still explicit and compiler-checked. + +A caller who bounds on the consumer trait sees only the capability, never the dependency types the provider happens to need, so a library can require an internal helper without publishing it. This is a targeted pitch for the **library author** and complements the [explicit-and-compiler-checked-dependencies selling point](selling-points.md), which now carries the encapsulation angle; it rests on [impl-side dependencies](../concepts/impl-side-dependencies.md). The honest limit: this is a narrower, more advanced benefit than the others here, so it belongs in a piece aimed at library authors rather than in a general hook, where it would land as abstract. + +## Choosing which problem to lead with + +The last decision is which of these to open on, and it is settled by the reader, not by which problem is most impressive. Name the one or two dominant profiles for the piece and its channel first, per [reader-profiles.md](reader-profiles.md), then pick the problem that reader feels most sharply: the mock-in-tests swap for the pragmatic majority, the orphan-rule escape for the trait-heavy developer, overlapping implementations for the type-system reader, the error-type swap for the systems and ML-module reader, the monolith decomposition for the evaluator, and the structure-generic framework for the tooling author. When a piece must serve a broad public audience, lead with the mock-in-tests swap or the orphan-rule escape, because they are the most widely felt and the least likely to read as astronaut architecture — and hold the more advanced problems for the channels where their reader gathers. diff --git a/docs/communication-strategy/reader-profiles.md b/docs/communication-strategy/reader-profiles.md new file mode 100644 index 00000000..aab7b7d5 --- /dev/null +++ b/docs/communication-strategy/reader-profiles.md @@ -0,0 +1,87 @@ +# Reader profiles + +This document profiles the kinds of readers that public-facing CGP writing must serve, so a writer can name who a piece is for and shape it to what that reader already knows, hopes for, and fears. + +## Why reader profiles matter + +Public writing about CGP lands only when it is written for a definite reader, because CGP's ideas are unfamiliar enough that one framing helps an audience while the same framing loses another. "Overlapping instances made safe" reads as a gift to a Haskeller and as a warning to a pragmatic Rust engineer that the crate is too clever. "No runtime container" reassures a systems programmer and confuses a Spring developer who cannot picture dependency injection without one. Naming the reader is therefore the first decision in any piece, ahead of the outline and the examples, because it decides which benefit to lead with, how deep to go, which analogies to reach for, and which misunderstanding to head off first. + +Two axes describe most readers, and they vary independently. The first is **how much Rust the reader knows**, from someone who learned the language a few months ago to someone fluent in higher-ranked trait bounds and type-level programming. The second is the **disposition** the reader brings: an enthusiast who is delighted by expressive, type-level machinery, against a pragmatist who is wary of complexity and distrusts "clever" code on sight. The axes are orthogonal — an expert can be either an enthusiast or a hardened skeptic — and in the Rust community the skeptic is both the more common and the harder reader to win, so writing that satisfies the skeptic tends to satisfy everyone. A softer third axis is the **mental model a reader imports** from another language or ecosystem, which decides which analogies land and which mislead; those backgrounds are covered in depth by the [related-work](../related-work/README.md) documents and summarized as reader traits below. + +Real readers are blends, so treat these profiles as lenses to combine rather than boxes to sort into. A single reader might be an advanced Rust developer, a former Haskeller, and a pragmatic skeptic all at once, and a piece aimed at them should draw on all three. For each profile below, the goal is to hold five things in view: who the reader is, what they already know, what excites them and so makes the hook, what makes them skeptical and so must be defused, and how a piece should be written to reach them. + +## Readers by Rust experience + +The clearest way to place a reader is by how much Rust they command, because that sets the ceiling on how much CGP machinery a piece can show before it stops teaching and starts alienating. The three profiles here span the range the writing must cover, and the enthusiast-versus-skeptic disposition cuts across all of them. + +### The Rust newcomer + +The newcomer learned Rust recently and is still absorbing ownership, borrowing, and the basics of traits, so most of CGP's machinery sits far over their current horizon. They are comfortable with structs, enums, and calling methods, they have met common traits like `Clone` and `Debug`, and they may still be fighting the borrow checker; generics-heavy or type-level code is not yet part of their vocabulary. What excites this reader is the promise that they do not have to master a new paradigm to benefit — that CGP code can *read* like ordinary functions and plain trait impls, that an `#[implicit]` argument looks like a normal parameter, and that they can adopt one small piece at a time. + +The risk with the newcomer is that the full paradigm buries them: the consumer/provider split, wiring tables, and any mention of coherence will overwhelm someone still forming their model of traits, and CGP's long generated-type error messages are demoralizing to a reader who cannot yet parse them. Write for this reader by leading with the gentlest on-ramp — a single capability defined as a function, a context that gains it with one wiring line — and keep the internals (`IsProviderFor`, `DelegateComponent`, the generated blanket impls) entirely out of sight. Reach for familiar analogies, such as a wiring table as a settings map, never open with the coherence problem, and say explicitly that the whole system need not be understood at once. The honest message is that CGP meets them where they are and grows with them. + +### The working Rust developer + +The working developer is fluent in traits, generics, and everyday patterns, ships production Rust, and judges any new tool by whether it earns the complexity it adds — this is the pragmatic majority of the Rust audience and the one most writing should target. They know trait objects, the common ecosystem crates, and the builder and newtype patterns; they are comfortable reading generic code but actively value simplicity and are allergic to over-abstraction. What excites this reader is a concrete, familiar pain that CGP removes: swapping a real implementation for a mock across tests and production without paying for `dyn` dispatch, escaping the orphan-rule newtype dance, or decoupling modules without pulling in a dependency-injection framework — shown on code that looks like code they already write. + +The working developer's skepticism is the decisive obstacle, because they have watched "enterprise" abstraction ruin codebases and will pattern-match CGP to that at the first sign of ceremony. Their questions are whether it is over-engineered, what it does to compile times, how one debugs generated code, how long a teammate takes to learn it, and whether it is a lock-in risk. Win this reader by leading with a problem rather than the paradigm, showing a before/after on realistic code, and being candid about cost and about when *not* to reach for CGP. Emphasize that wiring is resolved at compile time and erased — there is no runtime cost — and demonstrate the vanilla-looking idioms so the code does not read as alien machinery. Restraint earns this reader's trust; a single overclaim loses them, often permanently and loudly. + +### The advanced Rust developer + +The advanced developer commands generics, associated types, higher-ranked trait bounds, `PhantomData`, and the coherence and orphan rules, and reads CGP fluently as trait machinery — often with delight, sometimes with a critic's eye. They may already push Rust's type system in their own crates, so nothing about a blanket impl or a type-level table is foreign, and they can follow an expansion without hand-holding. The enthusiast among them is excited by the expressive power: many overlapping impls made legal at once, provider selection made per-context, structure encoded as type-level lists, and the fact that all of it is zero-cost and works on stable Rust. This reader can become CGP's strongest advocate, and they want the real mechanism, not a simplified picture. + +The critic among the advanced readers is skeptical for informed reasons, and those reasons deserve engagement rather than deflection: the opacity of macro-generated code against hand-written traits, the compile-time and type-checking cost, the quality of the error messages, whether the abstraction pays for itself over plain trait bounds, and the principled worry that coherence exists for a reason. Write for this reader by showing the machinery honestly — the desugaring, the [`DelegateComponent`](../reference/traits/delegate_component.md)/[`IsProviderFor`](../reference/traits/is_provider_for.md) mechanism, and the [coherence trade-off](../concepts/coherence.md) argued on its merits — and by treating the [comparison to type classes](../related-work/type-classes.md) as a real intellectual exchange. Do not dumb it down, but do not mistake fluency for approval: this reader still asks whether the complexity buys genuine modularity or mere novelty, and because they shape opinion in the community, honesty and depth matter more here than anywhere else. + +## Readers by prior mental model + +Many readers arrive already fluent in a paradigm that CGP resembles, and the fastest way to reach them is through vocabulary they already own. Each profile below names the disposition that background tends to bring and points to the [related-work](../related-work/README.md) document that carries the full, cited comparison; read here for who the reader is, and read there for the depth of the mapping. + +### The functional-programming and type-system practitioner + +This reader comes from Haskell, Scala, OCaml, PureScript, F#, or Lean, and holds type classes, implicit parameters, row types, effect handlers, and ML modules as native concepts. They are the audience most predisposed to become advocates, because CGP hands them things their own languages forbid or make fragile — overlapping and orphan instances made safe, per-context selection, and extensible records and variants in a systems language — and they often actively want more type-level power, not less. The way in is to map the vocabulary directly (a component is a class or signature, a provider is a first-class instance or structure, wiring is instance resolution made explicit, an impl-side dependency is a class constraint or an effect) and to lead with the pains coherence and the orphan rule cause them. + +The caution with this reader is that they are precise about their own terms and will notice a loose analogy immediately, and some will ask why not simply stay in the functional language whose ergonomics they prefer. Answer that honestly — CGP's value is bringing the idea to Rust's performance and ecosystem, not out-competing the source language on elegance. The full treatments live in [type classes](../related-work/type-classes.md), [implicit parameters](../related-work/implicit-parameters.md), [row polymorphism and extensible data](../related-work/row-polymorphism.md), [algebraic effects and handlers](../related-work/algebraic-effects.md), and [ML modules and modular implicits](../related-work/ml-modules.md). + +### The enterprise and dependency-injection developer + +This reader comes from Java, Kotlin, or C# and the Spring, Guice, or Dagger world, and thinks in interfaces and implementations, inversion of control, testability, and wiring an object graph. What excites them is compile-time-checked, reflection-free dependency injection with per-context choice — "Dagger taken further" — where dependencies are explicit, nothing unused reaches the binary, and a missing binding is a compile error rather than a startup exception. The vocabulary maps cleanly: a provider is a bean or binding, `delegate_components!` is the container configuration, an impl-side dependency is a constructor parameter, and [`check_components!`](../reference/macros/check_components.md) is the graph validation a container runs — the difference being when it runs. + +The one assumption to defuse immediately is the runtime container: this reader will imagine an object somewhere holding the graph and resolving it by reflection, and so will expect a runtime cost and a runtime failure mode that CGP does not have. Say plainly that CGP's container is the type system and its graph is a set of trait impls resolved during compilation. Lead with the hidden-dependency and `NullPointerException` complaints that drove their own community toward constructor injection, since CGP answers exactly those. The full treatment is in [dependency injection](../related-work/dependency-injection.md), with the object-model and vtable framing in [dynamic dispatch](../related-work/dynamic-dispatch.md). + +### The dynamic-language developer + +This reader comes from Python, Ruby, or JavaScript and values duck typing, flexibility, and rapid iteration. What excites them is that CGP provider code reads like duck-typed code — sending messages to a context it never names — while being checked so it cannot blow up at runtime: the decoupling of dynamic dispatch with none of its cost, and duck typing that cannot throw `NoMethodError`. The framing that resonates is that CGP is the mechanisms they already use, with the runtime taken out. + +The skepticism here is aimed at Rust itself as much as at CGP: this reader fears rigidity and verbosity, cares more about not fighting the compiler than about zero-cost, and can experience CGP's long compile-time errors as exactly the rigidity they came to a dynamic language to escape. Be honest that runtime malleability — plugins loaded at startup, heterogeneous collections, monkey-patching — is not what CGP is for, and frame its "late binding" as late to the *wiring site* rather than to runtime. The full treatment is in [dynamic dispatch](../related-work/dynamic-dispatch.md). + +### The framework, library, and tooling author + +This reader builds serialization, ORMs, configuration systems, editors, or test frameworks, and thinks about generic-over-structure code, compile times, binary size, and the derive macros and reflection those tools lean on. What excites them is reaching reflection's payoff — write the framework once and have it work over any user type — with none of reflection's runtime cost or stringly-typed failure, because CGP encodes a type's shape as type-level lists the trait system resolves against, checked when the code is written and able to recurse into each field's own type. + +The objections this reader raises are compile-time cost and error quality, that it works only on types that opted in by deriving CGP's machinery, and that it is not a runtime introspection facility. Meet them by not calling CGP "a reflection system" — a reader sold on that will look for an API to query and find trait bounds instead — and by framing it as compile-time structural reflection encoded as types, with the opt-in derive requirement and the absence of runtime introspection stated up front. The full treatment, including the comparison to Bevy, facet, and Rust's emerging compile-time reflection, is in [reflection](../related-work/reflection.md). Reflection is also a live, high-attention area right now — first-class compile-time reflection is an officially pursued Rust project goal with an early implementation already landed — which makes the topic timely but sharpens the honest framing: present CGP as available today and type-level, complementary to the built-in facility the language is building rather than a rival it will absorb, and see [attention-and-engagement.md](attention-and-engagement.md) for where this conversation is drawing attention. + +## Readers by role and point of contact + +Beyond expertise and background, a reader's role and where they first meet CGP decide how much attention they will give and what decision they are making. These two profiles matter disproportionately for the twin goals of public writing — gaining attention and minimizing misunderstanding — because they govern first impressions and adoption. + +### The first-contact skimmer + +This reader is scrolling a social feed, a link aggregator, or a chat channel, gives the piece a few seconds, and will form a snap judgment they may broadcast. What earns their attention is a sharp, concrete hook — a one-line value proposition or a striking before/after — that sounds novel and real at once. What loses them is anything that lets them pattern-match CGP to a category they already dismiss: "just another DI framework," "macro magic," "over-abstracted Rust," "just use traits." One phrase that reads as pretentious or hand-wavy and they bounce, or worse, dunk. + +Write for the skimmer by leading with the single clearest benefit on concrete code, preempting the obvious dismissals in the opening lines, and keeping jargon out of the first impression. Because this reader shapes CGP's public reputation out of all proportion to the time they spend, the whole strategy of gaining attention while minimizing misunderstanding lives or dies in the first few lines they see. + +### The evaluator and decision-maker + +This reader is a tech lead, staff engineer, or architect weighing whether to adopt CGP for a team or a product, and they read for risk as much as for benefit. The benefits that move them are maintainability, testability, decoupling, performance guarantees, and reduced boilerplate at scale, backed by evidence that it works in real systems. Their skepticism is about cost and risk they will own: team ramp-up and hiring, the maturity of a young paradigm and ecosystem, how the code is debugged, and the wisdom of betting a codebase on something novel. + +Reach this reader with candor rather than enthusiasm: be honest about maturity and cost, show that CGP is a superset of ordinary traits so it can be adopted incrementally and stepped back from, and state plainly where it fits and where it does not. Address the "can my team learn this" question head-on rather than letting it fester. Overselling is fatal here, because this reader's job is to discount hype. + +## What nearly every reader shares + +Across all of these profiles, a few things hold for almost every reader, and they are the highest-leverage points for any public piece. The universal question underneath every profile is *is this worth the complexity?* — and the only convincing answer is a concrete problem solved plus honest limits, never enthusiasm alone. Three misunderstandings recur across nearly every audience and should be preempted early: that CGP has runtime cost or a runtime container and graph, when its wiring is resolved at compile time and erased; that it is "just another dependency-injection framework" or "just macro magic," when it is a trait-level paradigm with static guarantees; and that using it requires understanding all of its internals, when its vanilla-looking idioms let it be adopted a little at a time. Naming and defusing these three is often the single most valuable move a piece can make. + +The default reaction to CGP is the reflex to pattern-match it to something already familiar and dismissible, so the writer's real job is to make the genuinely novel part legible before that reflex fires. A tone that carries the whole spectrum is concrete over abstract, problem-first, honest about trade-offs, and free of hype — and when a piece must serve a mixed public audience, it should default to satisfying the pragmatic skeptic and the first-contact skimmer, because they are the least forgiving and the most numerous, and writing that wins them rarely loses anyone else. + +## Using these profiles + +To put this document to work, name the one or two dominant profiles for the piece and its channel before writing anything else. Draw the opening hook from what excites that reader and the first objection to defuse from what makes them skeptical, and pull the vocabulary and analogies from the matching [related-work](../related-work/README.md) document so the comparison speaks the reader's own language. When the target is a broad public audience rather than a specific community, write to the skeptic and the skimmer first and let the enthusiast, who forgives more, come along. diff --git a/docs/communication-strategy/selling-points.md b/docs/communication-strategy/selling-points.md new file mode 100644 index 00000000..4a9b1716 --- /dev/null +++ b/docs/communication-strategy/selling-points.md @@ -0,0 +1,135 @@ +# Selling points + +This document catalogs the selling points CGP should advertise and, for each, the phrasings that make it land and the phrasings that backfire, so a writer can pick the right pitch for the right reader and word it in a way this audience will believe. + +## How to use these selling points + +A selling point is a true CGP capability, stated in the reader's own language, that answers a problem the reader already feels. That definition carries three obligations at once, and dropping any one turns a selling point into the kind of claim the Rust audience punishes. It must be **true**, because the readers most worth winning are the ones most able to catch an overclaim; it must be **framed for a reader**, because the same capability excites one audience and alarms another, as [reader-profiles.md](reader-profiles.md) lays out; and it must be **anchored to a pain**, because "look what CGP can do" persuades no one, while "here is the problem you have, gone" persuades the skeptic. Lead every piece from a concrete problem, reach for the selling point that removes it, and state it in the words below. + +Two habits make the difference between a pitch that lands and one that invites a pile-on. First, pair the advantage with its cost whenever the audience is a skeptic — the [skepticism](skepticism.md) document is the companion to this one, and a selling point delivered without acknowledging its matching objection reads as spin. Second, prefer the concrete over the grand: a one-line before/after on familiar code outperforms any adjective, and "there is no runtime cost" outperforms "blazingly fast." The phrasing guidance in each section below is not decoration; the wrong word on the right capability is how CGP gets pattern-matched to something the reader already dismisses. + +## The headline pitch + +CGP's one-line identity should say what it *is* and what it *costs* in a single breath, because the reader's first question is "what is this" and their immediate second is "what does it cost me." The value proposition that does both is that **CGP is modular programming for Rust: write many interchangeable implementations of an interface and choose between them per context, resolved entirely at compile time with zero runtime cost.** That sentence leads with the benefit (many implementations, chosen per context), names the mechanism honestly (it is a way of writing Rust, not a runtime), and closes the escape hatch a skeptic reaches for (no runtime cost). Shorter openers — "swappable implementations, wired per context, erased before it runs" — keep the same three beats. + +Resist the urge to lead with the machinery. Opening on the consumer/provider trait split, `DelegateComponent`, or coherence-bypassing loses every reader but the advanced enthusiast, and even they respond better to a problem first. The internals are what a curious reader graduates *into*, not what they should meet in the first sentence. Reserve the phrase "context-generic programming" for after the value has landed; as an opener it is a name the reader cannot yet decode, and a name is not a pitch. + +## Zero runtime cost + +The strongest broad selling point is that all of CGP's flexibility is resolved at compile time and erased before the program runs, so the modularity costs nothing where it matters. There is no container holding a graph, no reflection walking types, no vtable, and no dynamic dispatch; a wired call monomorphizes to a direct function call, and a provider a context does not use is not in the binary. This is the selling point that answers the working developer's first suspicion — that abstraction means overhead — and the one that separates CGP from the runtime dependency-injection and reflection tools other languages rely on, as the [dependency injection](../related-work/dependency-injection.md) and [reflection](../related-work/reflection.md) comparisons detail. + +Say it like this: + +- "Resolved at compile time and compiled away — a wired call is a direct call." +- "No runtime container, no reflection, no vtable; nothing about the wiring survives into the running program." +- "Dependency injection that costs nothing at runtime." +- "Unused providers are not in your binary." + +Avoid the phrasings that overreach or read as slogans. Skip "blazingly fast" and any comparative speed claim without a benchmark to cite; the honest and stronger claim is that there is no cost to compare, not that CGP wins a race. "Zero-cost abstraction" is accurate but worn, so prefer the concrete "compiled to a direct call." And never imply there is a runtime component "but a fast one" — the point is that there is none, and blurring that invites the exact runtime-cost objection the selling point exists to remove. + +## Swap implementations without the runtime penalty + +CGP's central capability is that one interface can have many interchangeable implementations, chosen per context — per deployment, per test, per environment — which is the everyday problem that drives developers to trait objects or dependency-injection frameworks in the first place. A context wires a production implementation; a test wires a mock; a second application wires a third choice; none of them pay for `dyn` dispatch, and none of them collide. For the working developer this is the most legible benefit, because the mock-in-tests, real-in-production split is a problem they have solved awkwardly before, and CGP solves it with a swap of one wiring line. + +Say it like this: + +- "One interface, many implementations — the context picks which one, and the choice is a line you can read." +- "Mock it in tests, run the real thing in production, with no trait objects and no runtime dispatch." +- "The same component resolves to different implementations in different contexts, with no conflict between them." + +Avoid implying the choice is automatic. CGP does not find the implementation for you — you name it in a wiring table — and a pitch that says "CGP picks the right one" sets up the reader to feel misled the first time they write a `delegate_components!` entry. Frame the explicitness as the feature it is: the choice is one greppable place, not a resolution search you have to reverse-engineer. This is the wording that also defuses the implicit-resolution skepticism covered in [skepticism](skepticism.md). + +## Dependencies that are explicit and compiler-checked + +CGP makes what a provider needs from its context explicit in its declarations and enforces it at compile time, which answers the loudest complaint against dependency-injection frameworks directly. A provider states its dependencies through `#[uses]` and `#[implicit]` rather than hiding them, so a reader sees what a component requires without spelunking, and a context that fails to satisfy a dependency is a compile error at the wiring site — not a startup exception or a `NullPointerException` in production. This is "the explicitness the Spring community learned to prefer, by default," and it lands hardest with the enterprise and dependency-injection reader profiled in [reader-profiles.md](reader-profiles.md). It also answers a pain the pure-Rust reader feels without any framework at all: because a provider's dependencies live in its own implementation rather than in a public trait's method signature, they never force internal types to become public — the encapsulation leak that hand-rolled trait-based dependency injection is prone to, documented with its source in [attention-and-engagement.md](attention-and-engagement.md). + +Say it like this: + +- "A provider's dependencies are stated where the implementation lives and checked by the compiler." +- "A missing dependency is a compile error at the wiring site, not a runtime surprise." +- "No hidden dependencies: what a component needs is in its signature, not buried in its body." +- "[`check_components!`](../reference/macros/check_components.md) is your container's startup validation — run at compile time instead of at boot." +- "Dependencies stay in the implementation, not the public interface, so they never leak internal types into your API the way a public generic trait does." + +Avoid overselling the verification as effortless. The check is something you write, and its error messages, though they name the missing dependency, are verbose — say so when the audience is technical, because pretending the diagnostics are pretty is exactly the kind of small dishonesty that costs trust. Frame the trade honestly: you write a check line, and in return the whole class of runtime wiring failures cannot occur. + +## It reads like ordinary Rust, and you adopt it gradually + +A decisive selling point for the cautious majority is that CGP code can read like ordinary functions and plain trait impls, and that it is a superset of normal traits you can adopt one piece at a time. An `#[implicit]` argument looks like a function parameter; a provider written with `#[cgp_impl]` looks like a trait impl; a consumer trait can be implemented directly on a type with no CGP machinery at all. Nothing forces a project to swallow the whole paradigm at once, and a codebase can use a single component in one corner and stay otherwise vanilla. This is the selling point that disarms the "too clever / all-or-nothing" fear the Rust community brings to any new abstraction. + +Say it like this: + +- "It's a superset of ordinary traits — start with one component and leave the rest of your code unchanged." +- "Providers read like normal impls; implicit arguments read like normal parameters." +- "Adopt it gradually; you never have to buy the whole paradigm to use part of it." + +Avoid two opposite mistakes. Do not claim "no boilerplate" — there is wiring, and the honest framing is that CGP *moves* boilerplate into one readable place, not that it erases it. And do not undersell by leading with the machinery that makes the gradual on-ramp possible; the reader wants to hear "it looks like the Rust you already write," not a tour of the desugaring that achieves it. + +## Overlapping and orphan implementations, made safe + +For the type-system and functional-programming audience, the standout selling point is that CGP makes legal the overlapping and orphan implementations their languages forbid or make fragile — and makes them safe by keeping every choice explicit and local. Because a provider implements a provider trait for its own marker type rather than for the context, the orphan rule and the overlap rule do not bite: a crate can define many implementations of one capability, and implement a capability for a type it does not own, without the newtype dance or the coherence contortions. The per-context wiring table is what keeps this safe rather than chaotic, so incoherence never means the indeterminism that makes Haskell's `INCOHERENT` a footgun. The full comparison lives in [type classes](../related-work/type-classes.md) and [bypassing coherence](../concepts/coherence.md). + +This selling point has a rare asset behind it: developers already reinvent CGP's mechanism by hand. Rust programmers who hit the "no two blanket impls may overlap" wall reach independently for the same zero-sized-marker-plus-helper-trait pattern CGP is built on, as [attention-and-engagement.md](attention-and-engagement.md) documents — so the pitch reminds a reader of a workaround they have written and would rather not maintain, not a capability they must be talked into wanting. Leading with that recognition ("you have written the three-line version of this") disarms the "over-engineered" reflex faster than any claim about expressiveness. + +Say it like this: + +- "Type classes without the orphan rule — overlapping instances made legal and safe." +- "Implement a capability for a type you don't own, with no newtype wrapper." +- "Overlapping implementations coexist because each is a distinct provider; the context picks one, explicitly and locally." +- "The incoherence is deliberate at the definition level and disciplined at the use site — every choice is a line in a table, never a silent resolution." + +Avoid claiming CGP is coherent, or promising global uniqueness. It deliberately drops global coherence for per-context choice, and a reader who expects "one instance per type, program-wide" will look for a guarantee CGP scopes rather than globalizes. Say plainly that uniqueness is *per context*, and frame the scoping as the point — it is what lets two contexts treat the same type two ways without conflict. + +## Generic over a type's structure, checked and free + +CGP can write code that is generic over a type's fields and variants — serialization, builders, visitors, conversions — without runtime reflection, which is the reflection payoff without the reflection cost. A type opts in with a derive, its shape becomes type-level data the trait system resolves against, and generic code recurses over that shape with full static checking and no runtime introspection, no stringly-typed field access, and nothing walked on every call. This is the selling point for the framework and tooling author, and the [reflection](../related-work/reflection.md) comparison carries the detail. + +Say it like this: + +- "Reflection's payoff without the runtime cost or the stringly-typed failures." +- "Write a framework once over any type that derives the shape — checked when you write it, not when it runs." +- "A type's structure becomes types the compiler resolves, not a descriptor you walk at runtime." + +Avoid calling CGP "a reflection system" or "reflection for Rust." A reader sold on that will look for an API to query a type and find trait bounds instead. The precise framing is "compile-time structural reflection encoded in the type system," and the honest limit — it works only on types that opted in via a derive, and does no runtime introspection — belongs in the same breath, per [skepticism](skepticism.md). + +## Abstract types chosen per context + +A quieter but powerful selling point is that CGP lets generic code name a type — an error type, a scalar, a runtime — that each context fills in for itself, through the same wiring that selects behavior. Code written against an abstract `Error` or `Runtime` works unchanged whether a context chooses `anyhow::Error` or a custom enum, Tokio or a mock, and swapping the concrete type is a wiring change that touches no provider. This unifies type selection and implementation selection into one mechanism, which resonates with the ML-module and abstract-type audience and underpins CGP's modular error handling. + +Say it like this: + +- "Generic code names the type; the context chooses it — the same wiring that picks behavior picks types." +- "Swap your error type or your runtime by changing one wiring line, touching no logic." +- "Abstract over the error type so fallible code never commits to a concrete one." + +Avoid conflating this with sealing or representation hiding. CGP's abstract types defer and configure a type; they do not hide a representation behind a type-system boundary the way an ML module's sealed type does. An ML-module reader will expect sealing, so say plainly that representation hiding is Rust's module privacy, separate from this feature — the [ML modules](../related-work/ml-modules.md) comparison makes the distinction. + +## It works on stable Rust today + +A plain but load-bearing selling point is that CGP is a library on stable Rust, not a language fork, an experimental feature, or a nightly-only trick. Much of what it delivers — type-class-style modularity, functor-style assembly, extensible data, the exactly-once fragment of effect handlers — corresponds to features that other ecosystems ship only in research languages or unmerged proposals, and CGP provides them as a crate you can add to a project now. This matters most to the evaluator weighing adoption risk and to the functional-programming reader who assumes this level of expressiveness requires a different language. + +Say it like this: + +- "A library on stable Rust — not a fork, not a nightly feature, not a proposal." +- "The expressiveness you'd reach for another language to get, available as a crate today." + +Avoid overstating maturity while making this point. "Works on stable Rust today" is true and worth saying; "production-proven at scale" is a different claim that needs evidence, and the evaluator reader will hear the gap. Keep this selling point to what it is — availability without a toolchain gamble — and let the maturity conversation happen honestly where [skepticism](skepticism.md) handles it. + +## Audience-tuned one-liners + +The sharpest phrasings are the ones that translate a selling point into the exact idiom of a reader's background, because they let the reader reuse everything they already know and spend their attention only on what is new. Each of the following is calibrated to one profile from [reader-profiles.md](reader-profiles.md) and grounded in the matching [related-work](../related-work/README.md) comparison; reach for the one that fits the piece's audience and avoid using it on a different audience, where it will misfire. + +- For the **Scala or implicits reader**: "implicits without the mystery — the dependency still arrives without threading it through every call, but which implementation supplies it is a line in a wiring table, not a resolution search." See [implicit parameters](../related-work/implicit-parameters.md). +- For the **Haskell or type-class reader**: "type classes without the orphan rule, and overlapping instances made legal." See [type classes](../related-work/type-classes.md). +- For the **Spring, Guice, or Dagger reader**: "Dagger, taken further — compile-time-checked injection, with per-context choice a single global binding graph can't express." See [dependency injection](../related-work/dependency-injection.md). +- For the **dynamic-language reader**: "duck typing that can't blow up at runtime, and dynamic dispatch that costs nothing." See [dynamic dispatch](../related-work/dynamic-dispatch.md). +- For the **OCaml or ML-module reader**: "functors with the plumbing automated — a declarative table instead of a hand-ordered functor chain." See [ML modules](../related-work/ml-modules.md). +- For the **algebraic-effects reader**: "effect handlers minus the continuation, resolved by type instead of dynamic scope, and extended to abstract types." See [algebraic effects](../related-work/algebraic-effects.md). +- For the **reflection or framework-author reader**: "compile-time reflection encoded in the type system — generic over a type's structure, checked when you write it, erased before it runs." See [reflection](../related-work/reflection.md). +- For the **PureScript or row-types reader**: "row polymorphism's extensibility, in a nominal systems language, opt-in per type." See [row polymorphism](../related-work/row-polymorphism.md). + +## Phrasing principles + +Across every selling point, a few wording rules hold, and they consolidate the per-section guidance into a checklist a writer can run before publishing. Prefer the concrete capability to the adjective — "compiled to a direct call" over "fast," "one interface with many implementations" over "flexible," "checked at compile time" over "safe" — because the concrete version is both more believable and more informative. State the honest limit in the same breath as the claim when the audience is skeptical, since the skeptic is scanning for the omission and trusts the writer who volunteers it. + +Certain words reliably cause the misunderstandings the [skepticism](skepticism.md) document exists to prevent, and they should be avoided by default. Do not say CGP "automatically resolves," "finds," or "figures out" the implementation — it is explicit, and this is the single most common misframing. Do not call it "magic," a word this audience treats as a warning rather than a wonder. Do not flatly call it "a DI framework," "a reflection system," or "an effect system" — each invites the reader to expect runtime behavior CGP does not have; qualify each with "compile-time" and the distinguishing limit. Do not promise "no boilerplate," "replaces traits," or any unbenchmarked "faster than"; the true, smaller claims — "moves boilerplate into one place," "a superset of traits," "no runtime cost" — are the ones that survive scrutiny and, with this audience, persuade more for surviving it. diff --git a/docs/communication-strategy/skepticism.md b/docs/communication-strategy/skepticism.md new file mode 100644 index 00000000..a565e5db --- /dev/null +++ b/docs/communication-strategy/skepticism.md @@ -0,0 +1,121 @@ +# Skepticism + +This document catalogs the skepticisms a reader brings to CGP, judges honestly whether each is justified, and prescribes the wording that answers it without triggering the misunderstanding that fed it. + +## How to use this document + +Skepticism is the default reaction to CGP, not the exception, so a writer's job is less to avoid objections than to meet them before the reader raises them. The skepticisms come from three sources, and telling them apart decides how to respond. Some are **imported** from a paradigm CGP resembles — the reader was burned by dependency-injection magic or spooky implicit resolution and transfers that wound to CGP on sight. Some are **native** to the Rust community's culture, which is proudly wary of complexity, macros, and abstraction that does not earn its keep. And some point at **genuine costs** CGP really carries, which no wording can spin away and which the writer must concede plainly. The worst mistake is treating a justified skepticism as a misunderstanding to argue down; the reader knows the difference, and the attempt destroys the trust the [selling-points](selling-points.md) rest on. + +For each skepticism below, the document names where it comes from, judges whether it is justified — fully, partly, or only through a misunderstanding — and gives the wording that responds. Two response patterns recur. When a skepticism rests on a **misunderstanding** (usually an assumption that CGP behaves at runtime like the thing it resembles), the fix is precise wording that heads off the wrong mental model before it forms. When a skepticism rests on a **real cost**, the fix is to concede it, then reframe it as the deliberate price of a benefit the reader values — the trade the reader would make anyway if they saw it whole. This document is the mirror of [selling-points.md](selling-points.md); most objections here answer a claim there, and the two must stay consistent so a writer is never told to say a phrase that the other warns against. + +## Skepticisms imported from other paradigms + +Many readers meet CGP already carrying a grievance against a concept it resembles, and their skepticism is really about that concept. The related-work documents record these grievances in depth and with citations; the task here is to recognize which imported wound is firing and to word CGP so the reader does not simply re-live it. In almost every case the reader's original complaint is *justified about the other tool* and *misapplied to CGP*, and the wording must grant the first while correcting the second — never by dismissing their experience, always by showing where CGP's design diverges from the thing that hurt them. + +### "This is dependency injection, and DI is heavy, magic, and fails at runtime" + +This skepticism comes from the enterprise reader's real scars: reflection-based containers that hide dependencies, fail at startup with an exception, cost time scanning a classpath, and behave as "magic" no one can debug. The complaint is thoroughly justified about the frameworks that provoked it, and entirely misapplied to CGP, which has no container, no reflection, and no runtime graph — its wiring is resolved by the compiler and compiled away, and a missing dependency is a compile error at the wiring site. The danger is that flatly calling CGP "dependency injection" makes the reader import the whole runtime picture, cost and failure mode included. + +Word it by naming the difference before the category. Say "compile-time, reflection-free dependency injection" rather than "dependency injection," and defuse the runtime-container assumption explicitly: there is no object holding a graph, the container is the type system, and resolution happens during compilation. Lead with the pains they resent — hidden dependencies, the startup `NullPointerException`, the "magic" they cannot trace — and show CGP answers each by construction. The framing that lands, per [dependency injection](../related-work/dependency-injection.md), is "Dagger, taken further": a reader who accepts Dagger's compile-time-checked, reflection-free injection already accepts CGP's bargain. + +### "Implicit resolution is spooky — values appear from nowhere" + +This comes from the Scala or Haskell reader who has spent an afternoon tracking down which `given` was selected or why an expected instance was not, and it is justified about implicit resolution: the mechanism's generality is exactly what makes it hard to follow. It does *not* transfer to CGP, but for a reason the writer must state carefully, because the obvious reassurance is a trap. CGP does not have spooky resolution because it does not resolve automatically at all — the provider is named explicitly in a wiring table. The misunderstanding to prevent here is the opposite of the usual one: the reader may hope CGP "just finds" the implementation, and it does not. + +Word it as "implicits without the mystery," then immediately anchor the honesty: the dependency still arrives without being threaded through every call, but *which* implementation supplies it is a line you can read, not the outcome of a search. Never promise automatic resolution — a reader sold on "CGP finds the right one" feels misled at the first `delegate_components!` entry, and the misframing also undercuts the coherence-freedom selling point that depends on there being no global search. The [implicit parameters](../related-work/implicit-parameters.md) comparison frames the trade: explicit wiring is the feature, not a shortfall. + +### "Coherence exists for a reason — incoherent instances are dangerous" + +This is the informed skepticism of the Haskell or advanced-Rust reader who knows that global coherence is what keeps a `Set` from being corrupted by two orderings, and that Haskell's `INCOHERENT` and Lean's diamonds let a wrong instance be chosen silently. It is justified in general — dropping coherence naively *is* dangerous — and it is the objection most worth engaging on its merits rather than deflecting, because the reader raising it is precisely the one who can become an advocate or a detractor. CGP's answer is that it drops global coherence but not safety: because every provider choice is explicit and per-context, incoherence never means the silent, searched-and-wrong selection that makes the extensions dangerous. + +Word it by conceding the general point and locating CGP's discipline precisely. Say that CGP is deliberately incoherent at the definition level and disciplined at the use site — overlapping providers coexist, but a context names exactly one, explicitly and locally, so the choice can never be silently derailed by overlap. Do not claim CGP is coherent, and do not promise program-wide uniqueness; say plainly that uniqueness is per context, and frame that scoping as the point. The [type classes](../related-work/type-classes.md) and [bypassing coherence](../concepts/coherence.md) documents carry the full argument, including the honest case where genuine global uniqueness is what a program wants and coherent type classes are the better tool. + +### "Extensible records and rows mean gigantic error messages and specialist complexity" + +This comes from the PureScript or Gleam reader who has seen row-unification errors clog a terminal and watched extensible records earn a reputation as a tool you regret reaching for. It is *partly justified* against CGP, and this is one to concede rather than wave away: CGP's structural operations, when mis-wired, do surface as long, generated-type-heavy trait errors — the CGP-idiom echo of the row-unification message that pushed two languages away from rows. What CGP changes is that the shape is opt-in per type rather than pervasive, and that [`check_components!`](../reference/macros/check_components.md) forces a missing field or variant to be named at the wiring site instead of deep inside a use. + +Word it by owning the diagnostic cost and bounding it. Say that CGP inherits verbose trait errors, that a check localizes them to the wiring site, and that because the structural view is opt-in you pay the complexity and the error-message tax only where you use the power — not across the whole program, as a row kind would impose. Do not claim the errors are clean; the reader has seen this movie and will trust the writer who admits the ending. The [row polymorphism](../related-work/row-polymorphism.md) comparison frames it as extensibility brought to a nominal language, paid for only where used. + +### "Isn't this just an effect system / a reflection system?" + +These two come from readers fluent in algebraic effects or in reflection, and both are misunderstandings that a careless pitch invites. The effects reader will look for continuations — `resume`, multi-shot handlers, generators, async — and CGP has none; a provider is an ordinary function that returns exactly once. The reflection reader will look for an API to query a type at runtime, and CGP has none; the structure is types the compiler resolves, and only for types that opted in via a derive. Neither skepticism is a real deficiency, because CGP never claimed to be either thing — but a writer who calls it "an effect system" or "reflection for Rust" creates the false expectation and then owns the reader's disappointment. + +Word it by naming what CGP is with precision and stating the limit up front. For effects: "the exactly-once, resume-in-place fragment of effect handlers — dynamic binding, made static and per-context," with the plain admission that continuations, generators, and async are outside it (see [algebraic effects](../related-work/algebraic-effects.md)). For reflection: "compile-time structural reflection encoded in the type system," with the plain admission that it does no runtime introspection and works only on types that derive the shape (see [reflection](../related-work/reflection.md)). In both cases the honesty is the persuasion: a reader who knows how much machinery exists to tame continuations, or how much runtime cost reflection carries, hears "we don't do that part" as a considered trade. + +### "Dynamic dispatch is flexible — won't a static version lose what makes it useful?" + +This comes from the dynamic-language reader who values runtime openness: heterogeneous collections, plugins loaded at startup, live redefinition, metaprogramming. Their skepticism is *justified and must be granted fully*, because CGP genuinely cannot do these things — it resolves at compile time, so there is no runtime object graph to mutate and no runtime dispatch to intercept. What CGP offers instead is the decoupling of dynamic dispatch with none of its cost or its runtime failure modes, which is a different bargain, not a replacement for runtime dynamism. + +Word it by drawing the line honestly and selling what is on CGP's side of it. Say plainly that runtime openness — plugins, heterogeneous collections, monkey-patching — lives on the runtime side, where Rust's own `dyn Trait` and the dynamic languages remain the right tools, and that CGP is for the case where the set of implementations is known at build time. Then offer the pair this reader most wishes for: "duck typing that can't throw at runtime" and "dynamic dispatch that costs nothing." Frame CGP's late binding as late to the *wiring site*, not to runtime — the flexibility is real but spent at compile time. The [dynamic dispatch](../related-work/dynamic-dispatch.md) comparison holds the full mapping. + +## Skepticisms native to the Rust audience + +Beyond the imported wounds, the Rust community brings its own well-earned wariness, and these skepticisms fire even for a reader with no functional-programming or enterprise background. This culture has watched abstraction ruin codebases and macros obscure behavior, and it treats "clever" as a criticism. Several of these skepticisms are partly or fully justified, and the writing must earn trust by conceding the true part rather than by insisting the complexity is free. + +### "This is too clever / over-engineered / astronaut architecture" + +This is the reflex of the pragmatic majority, and it is a reasonable prior rather than a misunderstanding: most abstractions that announce a new paradigm do not earn their cost, and the reader is right to demand that CGP prove it does. The skepticism is justified as a *default to be overcome with evidence*, not as a verdict. The failure mode is answering it with more enthusiasm about the paradigm, which confirms exactly the fear. + +Word the response as problem-first restraint. Lead with a concrete, familiar pain and show it gone in a before/after on ordinary-looking code; let the modularity justify itself by removing something real rather than by being impressive. Say explicitly when *not* to reach for CGP — a capability with one implementation does not need it — because naming the boundary is what proves the tool is not being oversold. The reader who sees the author decline to apply CGP everywhere believes the author about where it does belong. + +### "Macros are magic — I can't see what they generate or debug it" + +This skepticism is partly justified: CGP is macro-driven, and generated code is genuinely harder to inspect and step through than hand-written code, which the macro-wary Rust reader is right to weigh. What tempers it is that the expansions are documented and deterministic, the generated traits and impls are ordinary Rust once emitted, and a provider can be written and read without ever seeing the macro output. But the honest position concedes that debugging generated code is a real cost. + +Word it by replacing "magic" with "explicit" and pointing at the seams. Never use the word "magic" approvingly — this audience reads it as a warning — and instead emphasize that the wiring is a table you write and read, the dependencies are declared, and the expansion is specified. Concede that generated code is harder to trace, and offer the mitigations honestly: the expansions are documented, and the vanilla-looking idioms keep most code readable without expansion. Overclaiming transparency here loses the exact reader you are addressing. + +### "I can't tell which code actually runs on a method call" + +This is a specific, well-earned worry distinct from the macro complaint, and it appeared verbatim in CGP's own community discussion (see [attention-and-engagement.md](attention-and-engagement.md)): a reader fears that with wiring indirection it becomes "basically impossible to tell which particular bit of code will be entered on a method call," which makes a codebase hard to navigate. It is *partly justified* — CGP does add a layer between a consumer-trait call and the provider that answers it, so a reader tracing execution has one more hop to follow than with a direct call, and denying that indirection would be dishonest. + +Word it by conceding the hop and pointing at the map. The indirection is real, but unlike runtime dynamic dispatch it is *statically resolved and explicit*: the [`delegate_components!`](../reference/macros/delegate_components.md) table is a single, greppable place that names exactly one provider for each component, so "which code runs" is a table lookup that a reader — or an IDE's go-to-definition — follows at compile time, not a runtime search over registered handlers. Frame the wiring table as the navigation aid it is, the one location that answers "what implements this here," rather than pretending the indirection is absent. This is the same explicitness that answers the implicit-resolution and "magic" objections and that the [swap-implementations selling point](selling-points.md) calls "one greppable place": the choice is written down, in one spot, and can never be silently redirected. + +### "What does this do to compile times?" + +This is a justified and specific concern, not a reflex: trait-resolution-heavy code and macro expansion do add compile-time work, and a reader who has waited on a slow Rust build is right to ask. Pretending otherwise is the kind of dodge that ends trust. + +Word it by conceding the direction and declining to invent a magnitude. Acknowledge that CGP adds compile-time work, do not quote a number you cannot cite, and reframe against the runtime it replaces: the resolution that costs at compile time is resolution that would otherwise cost at runtime or not be checked at all. If a concrete measurement exists for the piece at hand, cite it; if not, keep the claim qualitative and honest rather than reassuring and unsupported. + +### "The error messages are a wall of generated types" + +This is justified, and it is the cost most likely to bite a real user, so it must be conceded without flinching. A mis-wired context can produce long, generated-type-heavy diagnostics, and a reader who has hit one will not believe a writer who pretends the experience is pleasant. + +Word it by owning the problem and naming the mitigation precisely. Say that CGP's errors can be verbose, that [`check_components!`](../reference/macros/check_components.md) exists specifically to force the real cause — a missing field, type, or dependency — to be named at the wiring site rather than surfacing far away, and that this localizes the failure even though it does not make the message short. This is honest and still reassuring: the tool has a known sharp edge and a known way to blunt it. Claiming the diagnostics are good is the fastest way to lose the technical reader. + +### "Rust doesn't need dependency injection — that's a Java-ism" + +This is distinct from the enterprise reader's imported fear of runtime DI magic, and it is the *native* Rust version: the pragmatist who has read that idiomatic Rust already does dependency injection with plain traits and generics, and who therefore treats the whole category — and any crate that markets itself with the term — as an unwanted import from the Java and Spring world. The sentiment is real and widespread, and it is *partly justified*: for a great many cases, passing a trait-bounded generic or a trait object genuinely is the right tool, and a DI *framework* would be over-engineering, which is why Rust's own DI crates stay niche (see [attention-and-engagement.md](attention-and-engagement.md)). The trap is that flatly pitching CGP as "dependency injection" invites this reader to file it under "solution to a problem Rust doesn't have" and move on. + +Word it by agreeing, not arguing. Concede that Rust does not need a DI *framework* and that CGP is not one — it is the traits-and-generics approach the reader already endorses, carried to the cases where doing it by hand stops scaling. Then locate those cases concretely: when a capability needs several interchangeable implementations chosen per context, plain generics hit the coherence wall that forces the marker-struct workaround developers already hand-roll ([attention-and-engagement.md](attention-and-engagement.md)); when the dependency lives on a type you do not own, the orphan rule blocks it; and when a public trait carries its dependencies in its signature, it leaks internal types into the public API, the encapsulation cost documented in [selling-points.md](selling-points.md). CGP earns its place at exactly those boundaries, and nowhere below them. The honest frame, per [dependency injection](../related-work/dependency-injection.md), is that CGP is the trait-based approach this reader already prefers, with the boilerplate and the coherence contortions removed — not a framework asking them to think like a Java developer. + +### "Why not just use traits, generics, or an enum?" + +This is the sharp, fair challenge of a competent Rust developer, and often the right question: for many problems, plain traits or an enum genuinely are the better tool, and CGP would be over-engineering. The skepticism is justified whenever the problem has one implementation or a closed, small set of them, and the writing must not pretend CGP wins universally. + +Word it by conceding the range where the plainer tool wins and drawing the line where CGP starts to pay. Say that a single implementation belongs in an ordinary trait, a closed variant set in an enum, and that CGP earns its cost when a capability needs several interchangeable implementations chosen per context, or an implementation for a type you do not own, or coherence-free overlap — the cases plain traits handle awkwardly or not at all. The [modularity hierarchy](../concepts/modularity-hierarchy.md) is the honest map of when to climb from a plain trait to a full component, and pointing to it signals that CGP is a rung to reach for deliberately, not a default. + +### "It's immature — can I bet a codebase on it?" + +This is the evaluator's central, justified concern, and it is about risk the reader would own: a young paradigm, an evolving ecosystem, and the cost of a team learning something novel. It cannot be answered by enthusiasm, and it should not be minimized. + +Word it with candor and with the escape hatches. Be honest that CGP is young and that adoption is a real decision, then lower the stakes truthfully: it is a superset of ordinary traits, so it can be adopted incrementally in one corner and stepped back from without rewriting a codebase, and it imposes no runtime, so it does not lock a project into a framework's lifecycle. Address the "can my team learn this" question directly rather than letting it linger. The evaluator discounts hype by profession, per [reader-profiles.md](reader-profiles.md), so the only register that works is level. + +### "There's a learning curve" + +This is justified and should simply be granted: the consumer/provider split, wiring, and coherence-bypassing are a genuine conceptual load, and a reader who suspects a ramp-up is right. Denying it insults the reader; the useful move is to shrink the *first* step rather than the whole curve. + +Word it by separating the on-ramp from the depth. Say that the first useful thing — a capability defined as a function, wired once — takes only ordinary Rust knowledge, and that the deeper machinery can be learned as needed rather than up front. Point the newcomer at the gentle path, not the full reference. The honest message is that the curve is real but not a cliff, and that a reader can be productive well before they understand the whole system. + +## A careful-wording checklist + +The skepticisms above share a small set of wording failures that reliably create or inflame them, and a writer can catch most problems by scanning a draft against this list before publishing. The through-line is that vague or grand wording invites the reader to supply the worst interpretation, while precise wording forecloses it — and that conceding a real cost in plain words buys more trust than any amount of polish. + +The words and moves to avoid, each paired with what to do instead, are worth keeping in view: + +- Do not say CGP "automatically resolves," "finds," or "figures out" an implementation. Say the provider is named explicitly, in one readable place — the explicitness is the feature. +- Do not use "magic," even admiringly. Say "explicit," and point at the wiring table and the declared dependencies. +- Do not flatly call CGP "a DI framework," "a reflection system," or "an effect system." Qualify each — "compile-time, reflection-free dependency injection," "compile-time structural reflection," "the dynamic-binding fragment of effect handlers" — and state the distinguishing limit in the same breath. +- Do not imply any runtime component. Say the wiring is resolved at compile time and erased; a wired call is a direct call. +- Do not claim "no boilerplate," "replaces traits," or an unbenchmarked "faster." Say "moves boilerplate into one place," "a superset of traits," and "no runtime cost." +- Do not pretend the diagnostics are clean or the compile-time cost is nil. Concede both, and name [`check_components!`](../reference/macros/check_components.md) and the compile-time-versus-runtime trade as the honest mitigations. +- Do not sell CGP as universally better than plain traits, or promise runtime dynamism it lacks. Name where the plainer tool or a runtime mechanism wins; the concession is what makes the rest believable. diff --git a/docs/communication-strategy/tag-lines.md b/docs/communication-strategy/tag-lines.md new file mode 100644 index 00000000..0818827c --- /dev/null +++ b/docs/communication-strategy/tag-lines.md @@ -0,0 +1,77 @@ +# Tag lines + +This document brainstorms the tag lines CGP could use to describe itself to the general public, weighing each for the attention it wins, the skepticism it triggers, and whether the claim is one CGP can honestly defend. + +## Why the tag line is worth this much thought + +The tag line is the first thing a reader learns about CGP and often the only thing, so it shapes the project's reception more than any document beneath it. It is the compressed form of everything in [selling-points.md](selling-points.md) and [skepticism.md](skepticism.md): in a handful of words it has to gain attention and minimize misunderstanding at once, for an audience — the [reader profiles](reader-profiles.md) — that ranges from a skimmer giving it three seconds to an expert who will pick the wording apart. A tag line that wins the skimmer but reads as an overclaim to the expert has failed, and so has one that satisfies the expert but says nothing a skimmer can act on. + +The incumbent tag line — CGP as "a modular programming paradigm for Rust" — has carried the project since its early days, but it now underperforms for two separable reasons. First, it undersells what CGP has become. The project began as a way to make trait implementations modular, but it has since accreted abstract types, extensible data, a handler and computation family, namespaces, and more, until it reads less like one technique and more like a broad extension to Rust itself. Second, and more damaging, its key word does not land. "Modularity" is an abstraction few developers wake up wanting, "paradigm" reads as academic, and for a large slice of the audience "modular" carries active baggage from runtime dependency-injection frameworks — the hidden dependencies, the configuration weight, the "magic" documented in [dependency injection](../related-work/dependency-injection.md) — so the word meant to attract instead signals over-engineering to the very pragmatists CGP most needs to win, the reflex catalogued in [skepticism.md](skepticism.md). A tag line whose lead word repels part of the audience on contact is working against the project. + +This document therefore treats the tag line as an open question. It sets out how to judge a candidate, works through the field honestly — including the "language of its own" alternative the project is considering — and lands on a recommended direction to validate rather than a decree, because tag-line efficacy is ultimately empirical. + +## How to judge a tag line + +A candidate is judged on three axes, and the third is the one that most often kills an otherwise attractive line. The first is **attention**: does it hook, and whom — a tag line that excites the type-system enthusiast may leave the pragmatist cold, and vice versa. The second is **skepticism**: what aversion or misunderstanding it triggers, and whom it repels — the loaded word, the overclaim, the frame that invites a dismissal. The third is **feasibility**, meaning honesty: is the claim accurate and defensible to the audience most able to check it, because the honesty-is-the-strategy rule of this section means a tag line that overclaims does more damage than a modest one, since the readers worth winning are the ones who will catch it. + +Three further rules break ties between candidates that score similarly. A **name is not a pitch**: a coined term tells a cold reader nothing, so it must be paired with a plain-language descriptor rather than shipped alone. A tag line can be **layered**: a short hook carries the attention and a subline carries the honest mechanism, so the two jobs need not fight for the same words. And the decisive test is that a tag line is **checked against the skeptic and the skimmer first**, because they are the least forgiving and most numerous readers in [reader-profiles.md](reader-profiles.md), and a line that survives them survives everyone. + +## The candidates + +The field below runs from the most conservative framing to the most ambitious, with the feature- and benefit-first options and the borrowed analogy in between. Each is weighed on the three axes and given a verdict. + +### The incumbent — "a modular programming paradigm for Rust" + +This line is accurate and safe, but the weakest on attention, and its lead word actively repels part of the audience. Its selling point is honesty and humility: it describes CGP as a way of writing rather than a runtime, and it resonates with the architecture-minded and dependency-injection-experienced reader who already values decoupling. Its skepticism cost is the one the project has already felt — "modularity" is abstract and, as the documented Spring sentiment shows, unloved or actively distrusted by developers who associate it with over-engineering, while "paradigm" reads as academic and conveys neither novelty nor a concrete pain. On feasibility it is unimpeachable: it overclaims nothing. The verdict is that it is *true but inert* — the safest possible descriptor and therefore worth keeping as a fallback, but the wrong thing to lead with, because its hook word is the one most likely to lose the pragmatist and the skimmer. + +### The bold alternative — "a language of its own, a superset of Rust" + +This is the ambitious reframing the project is weighing, and it splits sharply across the three axes. Its selling point is real: "superset of Rust" is intriguing enough to stop a skimmer, it honestly reflects how much CGP now adds, and it has a beloved precedent in TypeScript, whose "a superset that compiles down and adds an expressive type layer" positioning became one of the most successful in modern tooling. Under the TypeScript reading it can even *support* gradual adoption — "all your Rust still works, plus more." + +The skepticism cost is serious and comes in two parts. First, "a language of its own" scares the evaluator: it imports the full adoption-risk fear from [skepticism.md](skepticism.md) — a language to learn, tooling, ecosystem, hiring, lock-in — and reads as "throw away what you know," which fights CGP's strongest reassurance that it is a superset of ordinary traits adopted a little at a time. Second, the framing flips on which reading the reader takes: "superset" TypeScript-style (your code still works, plus more) is inviting, while "a language of its own" pushes the opposite, all-or-nothing reading, and the phrase invites the harsher one. + +Feasibility is where this candidate is weakest, and the point is concrete. CGP is a set of procedural macros and libraries that desugar to ordinary Rust and compile on a stable toolchain; it adds no grammar to the language. Taken literally, then, "a language of its own" and "a superset of Rust" are inaccurate, and the precise, pedantic slice of the Rust audience — a real and vocal group — will answer "it's not a language, it's a macro library," and a tag line that invites that correction has lost the exchange before it begins. The verdict is that the *ambition* is right and worth capturing, but the *words* overclaim; the honest carrier of the same ambition is "extension," below. + +### The honest upgrade — "a language extension for Rust" + +This is the defensible version of the ambition, and it keeps most of the upside while shedding the overclaim. Its selling point is that "extends Rust" still signals that CGP significantly enlarges what the language can express, matching the project's own observation that CGP now looks almost like a language extension, while "extension" implies addition-to rather than replacement-of, so it supports gradual adoption instead of fighting it. Its skepticism cost is milder than the bold version — some "do I have to learn a whole new thing" weight remains — and "extension" is a little vague on its own, so it needs a concrete completion: an extension that does *what*. On feasibility it is far more defensible, because an embedded, macro-based DSL is fairly called a language extension in ordinary usage, and pairing it with "on stable Rust, compiles to ordinary Rust" both stays honest and preempts the "it's just macros" dunk. The verdict is that this is the strongest *framing* of the ambition, provided it is completed with a concrete noun rather than left as a bare "extension." + +### Feature-first — "modular and reusable trait implementations for Rust" + +This line trades breadth for a concrete hook, and it is one of the safest strong candidates. Its selling point is that it ties CGP to a pain every Rust developer has felt: traits are rigid — one implementation per type, the orphan rule, no reuse of an impl across types — and "reusable trait implementations" is a promise the working developer can picture immediately, grounding the abstract idea of modularity in the trait system they use daily rather than leading with the bare word. Its skepticism cost is twofold: it still contains "modular," and it now risks *underselling* the breadth — abstract types, extensible data, the handler family — reproducing the incumbent's problem in a narrower form, which cuts against the project's sense that CGP has outgrown a trait-only story. On feasibility it is fully accurate. The verdict is that it is strong, concrete, and safe, and it is the best option whenever the hook can afford to be narrow and the breadth explained just below it. + +### Benefit-first, dropping "modular" — "write an implementation once, choose it per context" + +This family leads with a concrete outcome and sidesteps the loaded word entirely, which is its main virtue. Its selling point is that "write it once, swap it for tests versus production, chosen at compile time" names a pain the pragmatist recognizes and answers the runtime-DI fear in the same breath, without ever saying "modularity." Its skepticism cost is length and jargon: the phrasing is less punchy than a headline wants, and "context" is CGP's own term that a cold reader does not parse. On feasibility it is accurate. The verdict is that it is excellent as a *subline* — the honest mechanism beneath a shorter hook — but a little long and jargon-adjacent to carry the headline alone. + +### The category name — "context-generic programming" + +The project's own name is a candidate in its own right, and its role is specific. Its selling point is that it owns a unique, searchable term, that "generic programming" carries real respect in the Rust and C++ worlds, and that "generic over the context" is the literal technical thesis — coining a category can win long-term mindshare the way "reactive programming" did. Its skepticism cost is that it is opaque on first contact: a name is not a pitch, as [selling-points.md](selling-points.md) warns, and a skimmer learns nothing from it while an unkind reader hears it as academic. This cost is observed, not hypothetical — in CGP's own release discussions multiple readers said the phrase obscures more than it conveys and that "coining a new phrase makes it harder to understand," reaching instead for "structural typing" or "duck typing for statically-typed code" to describe what they thought CGP was. That both confirms the opacity and surfaces a plainer bridge term worth testing in body copy (with the caveat that CGP is nominal-and-wired, not truly structural); see [attention-and-engagement.md](attention-and-engagement.md). On feasibility it is perfectly accurate, being the name. The verdict is to keep it as *the name*, always paired with a plain-language descriptor, and never to ship it alone as the tag line. + +### The borrowed analogy — "TypeScript for Rust" + +An analogy can do the work of a paragraph in three words, and this one is tempting, but it does not survive as a tag line. Its selling point is instant legibility: it borrows TypeScript's proven "a superset that compiles down and adds an expressive layer, and it's still the language you know" positioning, which conveys both power and gradual adoption at once. Its skepticism cost is that the analogy misleads under scrutiny — TypeScript adds static typing to a *dynamically* typed language, whereas Rust is already statically typed, so what CGP adds is not what TypeScript adds, and the precise audience will reject the comparison. On feasibility it is low as a headline, because honesty-is-strategy warns against a frame that breaks the moment it is examined, though it is serviceable as a one-time explanatory analogy in body copy with the caveat stated. The verdict is: do not headline it; use it once, carefully, only to convey the superset-that-compiles-to-Rust intuition, then set it aside. + +### Audience-specific hooks — strong for a channel, wrong for the front page + +A separate class of phrasings are excellent hooks for one reader and poor master tag lines, and it is worth naming them so they are not misused. Lines like "overlapping trait impls, made safe," "type classes without the orphan rule," and "compile-time dependency injection without the framework" each land hard with one profile — the coherence-aware type-system reader, the Haskeller, the enterprise developer — and each is developed as an audience-tuned pitch in [selling-points.md](selling-points.md). But every one of them is either too niche for a cold general audience or, in the DI case, imports the exact baggage a general tag line must avoid. The verdict is to keep these for audience-targeted channels per [reader-profiles.md](reader-profiles.md) and never to make one the project's front-page line. + +## What the analysis points to + +Several findings fall out of the comparison, and together they describe a strategy rather than a single winning phrase. + +The first is to **retire "modular" as the lead word.** It is the weakest hook in the field and the only one that actively repels part of the audience, so modularity should survive as a supporting descriptor — and, when used, be immediately differentiated from its Spring connotations by pairing it with "compile-time," "zero-cost," and "explicit," the three properties that distinguish CGP from the runtime frameworks that gave "modular" its bad name. + +The second is to **capture the "extends Rust" ambition, but as "extension," never "a language" or "a superset."** The softer word is both the honest one — CGP is macros producing stable Rust, not a new grammar — and the pro-adoption one, since "extension" adds to what the reader knows while "a language of its own" tells them to leave it behind. The bold word overclaims to the audience most able to catch it and simultaneously fights CGP's best reassurance, which is a rare double loss. + +The third is to **lead with a concrete capability or the extension framing, complete it with a plain descriptor, and keep "context-generic programming" as the owned name beneath.** No single element does every job: the name owns the category but does not pitch, the capability pitches but does not convey breadth, the extension framing conveys breadth but needs a concrete completion. Layering them lets each do the job it is good at. + +The fourth is to **layer the line into a hook and a subline**, because the subline is where the skimmer's and skeptic's first objection is defused. The hook earns the click; the subline says "still Rust, checked at compile time, zero runtime cost" and heads off the runtime-container and new-language misreadings before they form. + +Drawing these together, the recommended shortlist below is a starting point to validate with the community, not a final choice, since which line performs best is an empirical question that only real audiences answer: + +- **Hook:** "Context-generic programming" — **subline:** "A Rust language extension for reusable, swappable trait implementations — checked at compile time, with zero runtime cost." This leads with the owned name, carries the ambition honestly through "extension," grounds it in the concrete "reusable, swappable trait implementations," and defuses the two big misreadings in the subline. +- **Hook:** "Reusable, swappable trait implementations for Rust" — **subline:** "A language extension that lets one interface have many implementations, chosen per context and resolved at compile time." This leads with the concrete capability for the pragmatist and skimmer, and uses the subline to convey the breadth and the mechanism. +- **Hook:** "Rust traits, extended" — **subline:** "Write an implementation once, reuse it across types, and choose it per context — all at compile time, still ordinary Rust." This is the punchiest, trading some precision in the hook for momentum, with the subline carrying the honesty. + +The lines to avoid are equally clear: do not lead with "a modular programming paradigm" (inert, and repels on its hook word), do not claim "a language of its own" or "a superset of Rust" (overclaims to the pedantic audience and fights gradual adoption), and do not headline "TypeScript for Rust" (a frame that breaks under scrutiny). Whatever the project chooses, the next step is to float the shortlist in the channels where the [reader profiles](reader-profiles.md) actually gather and watch which hook earns attention without drawing the "it's just macros" or "another DI framework" reply — because the tag line's real grade is the reaction it provokes, not the case made for it here. diff --git a/docs/communication-strategy/technical-barriers.md b/docs/communication-strategy/technical-barriers.md new file mode 100644 index 00000000..8bd958c7 --- /dev/null +++ b/docs/communication-strategy/technical-barriers.md @@ -0,0 +1,72 @@ +# Technical barriers + +This document maps the technical barriers a reader faces when trying to understand CGP, and the design affordances and teaching moves that lower each one, so that writing about CGP meets readers below the barrier rather than above it. + +## Comprehension is a separate problem from skepticism + +A reader can genuinely *want* to understand CGP and still fail to, because its concepts sit on a stack of Rust knowledge that not every developer has — and that comprehension barrier is a different problem from the one [skepticism.md](skepticism.md) treats. Skepticism is about *willingness*: a reader who understands CGP but doubts it is worth the complexity. A technical barrier is about *ability*: a reader who would happily use CGP but cannot follow the explanation, because it assumes a concept they never absorbed. A piece of writing has to clear both, and confusing them is a common failure — answering an ability problem with a persuasion argument leaves the reader no better able to read the next line. + +The encouraging fact is that CGP's own design already lowers most of these barriers, because its ergonomic constructs were introduced precisely to let a reader use a capability without first understanding the harder concept beneath it. That turns much of the communication job into *using* those affordances well: teaching the forms that look like ordinary Rust first, and revealing the machinery only when a reader has a reason to want it. And doing so is not a simplification that misleads — the ergonomic forms are the *recommended* way to write CGP, so leading with them shows the idiom rather than a beginner's dialect the reader must later unlearn. + +The single most important teaching move that follows is **progressive disclosure**: reveal complexity only as a reader needs it, lead with the vanilla-looking forms, and defer the underlying machinery until it is asked for. The [reader profiles](reader-profiles.md) make this concrete — the newcomer should see only the on-ramp, while the advanced reader is handed the internals directly — and every barrier below is eased by the same discipline applied to a specific concept. + +## The prerequisite ladder + +The barriers form a ladder of prerequisite Rust knowledge, from the concepts most readers lack to the ones only advanced readers hold, and knowing where a reader stands on it decides how much a piece can safely show. Near the bottom sit **generic parameters** and **traits with bounds**, which a surprising share of working Rust developers are uneasy with; above them come **blanket implementations** and **associated types**, which many have used but few reason about fluently; and near the top sit **coherence and the orphan rule** and **type-level programming with higher-ranked trait bounds**, which only the advanced audience holds. The recurring mistake is to assume the reader stands higher on this ladder than they do, because CGP's authors and its most enthusiastic early readers stand near the top and forget how much of the audience does not. The rest of this document takes the load-bearing barriers in turn, names the design affordance that hides each, and gives the teaching move that eases it. + +## Generic parameters are intimidating + +The largest barrier by far is that many Rust developers are uncomfortable with generic parameters and disengage the moment they see a signature bristling with `` and bounds. Written naively, CGP is generic-heavy — provider traits carry an explicit context parameter, methods thread `Code` and `Input` type parameters — so a reader who bounces on generics bounces on CGP immediately, before any of its value has a chance to land. + +CGP's design answers this with a set of constructs that let a reader write and read real CGP with no generic parameter in sight. [`#[cgp_fn]`](../reference/macros/cgp_fn.md) turns a capability into a plain function whose simplest form shows no generics at all; [`#[cgp_impl]`](../reference/macros/cgp_impl.md) lets a provider omit the explicit context parameter and the `for Context` clause; and [`#[implicit]`](../reference/attributes/implicit.md) makes the values a provider draws from its context look like ordinary function arguments rather than generic bounds. The teaching move is to lead every introduction with these forms, introduce a generic parameter only at the first moment it earns its place, and never open with a raw provider-trait signature. Show the reader that CGP code can look like the Rust they already write, rather than reassuring them that the generics are not as scary as they look — the demonstration persuades where the reassurance does not. + +## The provider trait reads inside-out + +A distinct and disorienting barrier is that a provider trait, in its raw form, turns Rust's conventions inside-out: the original `Self` moves to a zero-sized provider marker, the context becomes an explicit type parameter, and so `self` and `Self` no longer mean what a Rust programmer's instinct says they mean. A reader meeting this cold has to hold an inversion in their head before they can read a single provider body, and it is one of the most common places understanding stalls. + +[`#[cgp_impl]`](../reference/macros/cgp_impl.md) exists to remove this barrier: it keeps `self` and `Self` meaning the context, lets the author omit the `for Context` clause, and puts the provider name in the attribute rather than the `Self` position, so a provider reads like an ordinary trait impl. The teaching move is to present `#[cgp_impl]` as *the* way to write a provider and to keep the inside-out [`#[cgp_provider]`](../reference/macros/cgp_provider.md) shape out of introductory material entirely; when the raw form must be shown to explain the internals, say plainly and up front that here `self` and `Self` are the context. Introducing the [consumer/provider split](../concepts/consumer-and-provider-traits.md) by leading with the raw provider trait is the surest way to lose a reader who would have followed the `#[cgp_impl]` form without trouble. + +## Traits and bounds on the context are a barrier of their own + +Below the provider inversion sits a plainer barrier: some readers are shaky on traits at all, and even those who are fluent find a `where Self: SomeTrait` bound unusual, because bounding the implementing type is uncommon in everyday Rust and reads as machinery rather than intent. A reader who cannot comfortably parse "this code requires the context to implement this trait" cannot follow how a provider states what it needs. + +Two constructs lower this. [`#[cgp_fn]`](../reference/macros/cgp_fn.md) needs no trait at all — the reader writes a function and never encounters a trait definition — which is why it is the gentlest possible entry point. And [`#[uses]`](../reference/attributes/uses.md) turns a `Self: Trait` dependency into a line that reads like a `use` import of a capability, so the reader declares what a provider depends on without confronting the fact that it is a bound on the generic context. The teaching move is to start the least experienced reader on `#[cgp_fn]` with no mention of traits, and to introduce `#[uses]` as "importing a capability the code relies on," deferring the underlying `where`-clause reality until the reader is ready to care about it. + +## Traits that are "magically" implemented, and their imports + +A subtler barrier appears once a reader meets a getter trait: [`#[cgp_auto_getter]`](../reference/macros/cgp_auto_getter.md) generates a blanket implementation, so the trait is satisfied for a context without the reader writing any impl, and the method it adds is callable only when the trait is in scope. Both halves confuse a newcomer — a trait that implements itself by unseen machinery, and a method that exists only after an import they did not know to add — and together they make the reader feel the system is doing things behind their back. + +[`#[implicit]`](../reference/attributes/implicit.md) sidesteps the whole tangle: a value is read from a context field as an ordinary function argument, with no getter trait to define, no blanket impl to reason about, and nothing to import. The teaching move is to make implicit arguments the default way a reader learns to pull values from a context, and to introduce getter traits only for the narrow cases an implicit argument cannot reach — a field on another type, a named shared capability — and only once the reader is past the intimidation stage. Leading with `#[cgp_auto_getter]` forces the "how is this implemented, and why can't I call it" question at exactly the wrong moment. + +## Associated types and qualified paths + +Abstract types raise a comprehension barrier through their syntax as much as their concept: referring to a context's abstract error means writing `Self::Error`, or in full `::Error`, and associated-type paths of that shape intimidate readers and clutter the code they appear in. A reader who is still unsure about generics is not ready to parse a fully-qualified associated-type projection in a return type. + +[`#[use_type]`](../reference/attributes/use_type.md) removes this by letting the reader import an abstract type and then write the bare name — `Error`, `Scalar` — with the qualified path filled in and the supertrait or bound added automatically. It reads like a `use` for a type, which is a mental model the reader already owns. The teaching move is to present `#[use_type]` exactly that way, as importing a type, and to keep hand-written `Self::`-qualified associated-type syntax out of introductory material, bringing it in only when explaining a construct's own local associated type, where it genuinely belongs. As with `#[uses]`, the value is that the reader states a dependency on a type without first having to understand that it is a bound on a generic context. + +## The wiring table and its machinery + +Wiring raises a barrier of abstraction: the mental model of a type-level lookup table, the [`DelegateComponent`](../reference/traits/delegate_component.md) trait, and the component-name markers are all machinery a reader must otherwise absorb before they can believe a context "has" a capability. Shown the underlying traits, a newcomer sees an intimidating type-level construction where they expected a simple choice. + +The design keeps the surface simple — a [`delegate_components!`](../reference/macros/delegate_components.md) table is a compact list matching each capability to the implementation that supplies it, and [`delegate_and_check_components!`](../reference/macros/delegate_and_check_components.md) folds in the verification so a beginner cannot forget it. The teaching move is to describe the table with a plain analogy — a settings map, a lookup table pairing each component with a chosen provider — and to keep `DelegateComponent` and [`IsProviderFor`](../reference/traits/is_provider_for.md) out of the beginner's view entirely, since they are the mechanism, not the model. Mentioning that the table is resolved at compile time and compiles away doubles as the reassurance that this abstraction has no runtime cost. + +## Reading the error messages + +A barrier that design can lower but not remove is the error message: a mis-wired context can produce a wall of generated types, and understanding that wall requires exactly the machinery a beginner is trying to avoid. This is where a reader who was following comfortably can be thrown, because the failure speaks in a register the ergonomic surface had spared them. + +CGP's mitigation is real but partial. [`check_components!`](../reference/macros/check_components.md) forces the failure to surface at the wiring site and names the actual missing dependency — a field, a type, a capability — rather than letting it erupt far away, and the [errors catalog](../errors/README.md) documents the recurring shapes. The teaching move is to teach the reader to run a check and read the root cause first, and to set the expectation honestly: CGP's diagnostics can be verbose, a check localizes them, and that is the current state of the art rather than a solved problem. This is the one barrier where pretending it away costs more trust than admitting it, which is why [skepticism.md](skepticism.md) treats the same point from the persuasion side. + +## Knowing where to start, and why it is worth it + +Two meta-barriers sit above the individual constructs. The first is breadth: CGP has many pieces, and a reader who cannot see the minimal path assumes they must learn all of it before writing anything, which is its own reason to give up. The [modularity hierarchy](../concepts/modularity-hierarchy.md) is the answer to hand them — start at the lowest rung that solves the problem, a plain function or blanket trait before a full component — and a piece should give an explicit on-ramp rather than a tour of the whole surface. The second is motivation: a reader who can parse the syntax may still not grasp *why* the consumer/provider split earns its keep, so the mechanics read as ceremony. The move there is to lead with a concrete problem the reader has felt — a mock swapped for a real implementation, the orphan-rule newtype dance, a trait grown monolithic — and to defer the coherence theory that explains the split at a deeper level, exactly as [tag-lines.md](tag-lines.md) warns against opening on [coherence](../concepts/coherence.md). + +## The teaching strategy this points to + +The barriers above share one strategy, and a writer can apply it as a short checklist before publishing anything instructional. The principles reinforce each other, so the list is framed as a single discipline rather than independent tips: + +- **Disclose progressively.** Lead with the forms that look like ordinary Rust — `#[cgp_fn]`, `#[cgp_impl]`, `#[implicit]`, `#[uses]`, `#[use_type]` — and reveal the machinery beneath them only when a reader needs it, matching the depth to the reader profile. +- **Teach the ergonomic idioms as the idiom.** They are the recommended way to write CGP, not a simplified dialect, so leading with them is honest as well as gentle; keep the explicit and legacy forms for explaining internals and reading existing code. +- **Motivate before mechanism.** Open on a concrete problem the reader already has, not on the consumer/provider split or coherence; the mechanism lands only after the reader wants it. +- **Introduce vocabulary gradually and by analogy.** Defer the intimidating words — generic, blanket impl, coherence, higher-ranked trait bound, monomorphization — until they are needed, and anchor the ones you do use to a familiar picture, such as a wiring table as a settings map. +- **Show rather than reassure.** Demonstrating that CGP code can look like plain Rust convinces a nervous reader; telling them "it's not that hard" does not. +- **Be honest about the barrier design cannot remove.** Set expectations for verbose error messages and point at the check that localizes them, rather than implying the diagnostics are as smooth as the surface syntax. diff --git a/docs/communication-strategy/vocabulary.md b/docs/communication-strategy/vocabulary.md new file mode 100644 index 00000000..77e24110 --- /dev/null +++ b/docs/communication-strategy/vocabulary.md @@ -0,0 +1,52 @@ +# Vocabulary and message discipline + +This document is the canonical word list for public writing about CGP: the term to use for each concept, the plain-language gloss that introduces it to a cold reader, and the words and framings to avoid — so that everything written about CGP reads as one voice. + +## Why a shared word list matters + +CGP's public writing succeeds or fails on wording as much as on substance, so the section's "one voice" goal needs a single place that fixes the words rather than leaving each document to choose its own. The other documents each carry their own "say it like this / avoid this" guidance for their topic; this document consolidates those choices into a reference a writer checks before publishing, and it is the authority the others defer to — when a phrasing rule here and a phrasing rule elsewhere disagree, this document is where the disagreement is resolved. It is deliberately narrow: it governs *which word* to use, not *what to pitch* (that is [selling-points.md](selling-points.md)) or *how to answer an objection* (that is [skepticism.md](skepticism.md)). + +The list divides into three parts, and the division is the whole method. There are terms to **use** — the established CGP vocabulary, each paired with the one-line gloss that makes it legible to a reader meeting it for the first time. There are terms to **defer** — accurate internal words that intimidate a newcomer and should be withheld or introduced by analogy until the reader is ready, per [technical-barriers.md](technical-barriers.md). And there are words and framings to **avoid** — the ones that reliably trigger the misreadings [skepticism.md](skepticism.md) exists to prevent. A writer keeps the code and construct names accurate throughout, because a factual slip is shown to the audience most able to catch it, but chooses which of the accurate terms a given reader is ready for. + +## Terms to use, and how to introduce each + +The established CGP vocabulary is the same in public writing as in the rest of the knowledge base, so a reader moving between a blog post and the reference never reconciles two dialects. What public writing adds is the gloss: a term should be introduced with a plain-language definition on first use, then used consistently. Prefer these terms and these introductions. + +- **Context** — the concrete type an application wires and calls methods on. Introduce it as "the type that owns the wiring — your application, your test harness, your service," not as a bare "`Self`," because a cold reader has no reason to know the two coincide. +- **Component** — one capability, defined once, that can have many implementations. Introduce it as "an interface you can wire an implementation for," and reserve the internal detail that a component is a consumer trait plus a provider trait for when the reader asks how it works. +- **Consumer trait** and **provider trait** — the trait you *call* and the trait you *implement*. Introduce the pair only when a piece goes past the surface; for an introductory audience, "the trait you use" and "the code that implements it" carry the idea without the vocabulary. +- **Provider** — a named, swappable implementation of a component. Introduce it as "one implementation you can choose," which is the word's whole job; avoid explaining that it is a zero-sized marker type until the reader is reading generated code. +- **Wiring** — choosing which provider implements each component for a context. Introduce it as "a small table that says which implementation to use," and lean on the table image, since it is the single most load-bearing analogy in CGP writing. +- **Impl-side dependency** — a requirement a provider states in its own implementation rather than in the interface callers see. Introduce it as "the provider declares what it needs, and callers never see it," because the encapsulation benefit is the point and the phrase "impl-side" means nothing cold. +- **Context-generic programming** — the name of the paradigm, never the pitch. Introduce it only after a concrete capability has landed, always paired with a plain descriptor, per [tag-lines.md](tag-lines.md). + +## Terms to defer, and how to reveal them + +Some accurate terms are barriers, not vocabulary, for a reader below them on the [prerequisite ladder](technical-barriers.md), and leading with one loses the reader before the value lands. Defer these, and reveal each only when the reader has a reason to want it — matching the depth to the profile, so the advanced reader gets the real word immediately and the newcomer meets it late or by analogy. + +- **Coherence** and the **orphan rule** — the reason the consumer/provider split exists, but a theory the reader does not need to adopt CGP. Defer them behind the concrete pain (implementing a trait for a type you don't own), and introduce them, when at all, through that pain rather than as a rule to learn; a piece should never open on coherence, as [tag-lines.md](tag-lines.md) warns. +- **Blanket implementation**, **monomorphization**, **higher-ranked trait bound**, **`PhantomData`** — the machinery under the ergonomic surface. Withhold them from introductory material; when the runtime-cost question makes monomorphization worth naming, introduce it as "the compiler generates a direct call," not as jargon. +- **`DelegateComponent`**, **`IsProviderFor`**, the **type-level table** — the mechanism, not the model. Keep them out of a beginner's view entirely and describe the table with the settings-map analogy; bring the real traits in only for a reader reading an expansion or a compiler error, per [technical-barriers.md](technical-barriers.md). + +## Words and framings to avoid + +A handful of words reliably create the misunderstandings the section spends its effort preventing, and they should be avoided by default, each with a truer replacement that is also more persuasive to this audience. The through-line is that vague or grand wording lets the reader supply the worst reading, while a precise, smaller claim forecloses it and survives scrutiny — which, with the Rust audience, persuades *because* it survives. The pairs below consolidate the guidance scattered across the section into one checklist. + +- Avoid **"magic"**, even admiringly — this audience reads it as a warning. Say **"explicit,"** and point at the wiring table and the declared dependencies. +- Avoid **"automatically resolves," "finds," or "figures out"** the implementation — the single most common misframing, and the one that undercuts the coherence-freedom story. Say the provider is **"named explicitly, in one readable place."** +- Avoid flatly calling CGP **"a DI framework," "a reflection system,"** or **"an effect system"** — each makes the reader expect runtime behavior CGP does not have. Qualify each — **"compile-time, reflection-free dependency injection,"** **"compile-time structural reflection,"** **"the dynamic-binding fragment of effect handlers"** — and state the distinguishing limit in the same breath. +- Avoid implying **any runtime component**, even "a fast one." Say the wiring is **"resolved at compile time and compiled to a direct call."** +- Avoid **"zero-cost abstraction"** as a lead — it is accurate but worn. Prefer the concrete **"compiled to a direct call, with no vtable and nothing in the binary for a provider you don't use."** +- Avoid **"blazingly fast"** and any **unbenchmarked "faster than."** Say **"there is no runtime cost to compare,"** which is the honest and stronger claim. +- Avoid **"no boilerplate."** Say CGP **"moves the wiring into one readable place,"** because there is wiring and the reader will find it. +- Avoid **"replaces traits"** or **"a new language."** Say **"a superset of ordinary traits"** and **"a library on stable Rust," "an extension,"** never "a language of its own," per [tag-lines.md](tag-lines.md). +- Avoid **"just"** in a competitor's description — "just macros," "just another DI framework" are the reader's dismissals, not ours, and echoing them concedes the frame. +- Avoid overstating maturity — **"works on stable Rust today"** is true and worth saying, while **"production-proven at scale"** needs evidence the [evaluator](reader-profiles.md) will notice is missing. + +## The name, and the community's bridge terms + +The project's own name needs its own rule, because it is the term most likely to be misused as a pitch. Never ship **"context-generic programming"** as a standalone hook: it is opaque on first contact, and this is observed rather than hypothetical — readers in CGP's own release discussions said the phrase obscures more than it conveys and reached instead for "structural typing" or "duck typing for statically-typed code" to name what they thought it was ([attention-and-engagement.md](attention-and-engagement.md)). Always pair the name with a plain descriptor, and treat those community-supplied phrases as *bridge terms* for body copy — useful for meeting a reader where they are, but qualified, because CGP is nominal-and-wired rather than truly structural, and a precise reader will catch an unqualified "structural typing." Keep the name as the owned category term, per [tag-lines.md](tag-lines.md), and let a concrete capability carry the hook. + +## Keeping the list in sync + +Because this document consolidates wording rules the other documents also carry, it must stay consistent with them, and the coupling runs both ways. When a phrasing rule changes here, check the "say it like this / avoid this" lists in [selling-points.md](selling-points.md) and [skepticism.md](skepticism.md) and the title rules in [key-features.md](key-features.md) and [tag-lines.md](tag-lines.md), because a writer told to prefer a phrase in one place must never be warned against it in another. And when a CGP construct is renamed or a capability changes, the terms here are bound by the synchronization rule exactly as a reference document is: verify each against the source and the `/cgp` skill, and prefer the modern idioms the skill and the [guides](../guides/README.md) teach over the legacy forms a reader will still meet in older code. diff --git a/docs/communication-strategy/worked-examples.md b/docs/communication-strategy/worked-examples.md new file mode 100644 index 00000000..ed07d6f8 --- /dev/null +++ b/docs/communication-strategy/worked-examples.md @@ -0,0 +1,171 @@ +# Worked examples: annotated model drafts + +This document assembles the section's guidance into finished pieces of public writing — a launch post, a README above the fold, a social thread — and annotates each so a writer can see every decision and trace it to the document that argues for it. + +## How to read these drafts + +These drafts exist because the rest of the section is prescriptive and abstract, and a marketing-naive expert learns faster from one finished model than from twelve documents of rules. Each is a model of one artifact from [formats.md](formats.md), written the way that format's playbook prescribes, then followed by a note that names each move and its source. Read the draft first as a reader would, front to back and fast; then read the annotations to see the machinery behind it. The point is not to copy the copy but to watch the whole apparatus operate at once, so you can run it yourself on your own piece. + +Treat every draft as an illustration, not approved copy, because two honesty caveats travel with each one. The tag lines they use are the candidates from [tag-lines.md](tag-lines.md) that the project has still to validate with real audiences, so they are shown as plausible starting points rather than settled choices — [attention-and-engagement.md](attention-and-engagement.md) explains why publication itself is the real measurement. And every CGP claim and code snippet is bound by the knowledge base's synchronization rule exactly as a selling point is: verify it against the source and the `/cgp` skill before it ships, because a stale claim in a model draft is copied straight into real copy. + +The code uses the modern idioms the `/cgp` skill teaches, so a reader who borrows a draft learns the current idiom rather than a dialect to unlearn. A provider is written with [`#[cgp_impl]`](../reference/macros/cgp_impl.md), a value is read with [`#[cgp_auto_getter]`](../reference/macros/cgp_auto_getter.md) or an [`#[implicit]`](../reference/attributes/implicit.md) argument, and wiring is a [`delegate_components!`](../reference/macros/delegate_components.md) table — the same forms [problems-solved.md](problems-solved.md) uses, and for the same reason. + +## The link-aggregator launch post + +The launch post on Lobsters or the Rust subreddit is the format to get right first, because it is where CGP is actually discussed and its reader is the pragmatic skimmer who forms and broadcasts a snap judgment ([attention-and-engagement.md](attention-and-engagement.md)). The draft below leads with a pain, shows the workaround the reader already knows, and concedes the boundary in the same breath, per the [launch-post playbook](formats.md). + +### The draft + +> **Reusable, swappable trait implementations for Rust** +> +> Every Rust project eventually needs to swap a real implementation for a fake one — the real email sender in production, a recording stub in tests. The usual routes each cost something. A `Box` pays for dynamic dispatch and spreads a trait object through your types. A generic `` parameter has to be threaded through every layer that touches it, and it multiplies as more dependencies join. A dependency-injection crate brings machinery most of us would rather not. +> +> CGP makes the swap a single line. You define the capability once: + +```rust +#[cgp_component(EmailSender)] +pub trait CanSendEmail { + fn send_email(&self, to: &str, body: &str); +} +``` + +> write as many implementations as you need, each an ordinary-looking impl: + +```rust +#[cgp_impl(new SendViaSmtp)] +impl EmailSender { /* connect and send over SMTP */ } + +#[cgp_impl(new RecordEmails)] +impl EmailSender { /* record each message so a test can assert on it */ } +``` + +> and let each context pick the one it wants: + +```rust +delegate_components! { App { EmailSenderComponent: SendViaSmtp } } +delegate_components! { TestApp { EmailSenderComponent: RecordEmails } } +``` + +> `App` sends real mail; `TestApp` records it. Neither pays for `dyn`, the choice is one greppable line, and the code that calls `self.send_email(..)` never changes. +> +> To be clear about the cost: this is more than a single trait needs. For a dependency with one implementation, a plain trait is still the right tool, and CGP would be over-engineering. It earns its keep when the implementations multiply, when the choice must differ per context, or when you're implementing a trait for a type you don't own and the orphan rule blocks you. All the wiring is resolved at compile time and compiled to direct calls — there is no runtime container, no reflection, and nothing left in the binary for a provider you don't use. +> +> It's a library on stable Rust, and it's a superset of ordinary traits, so you can use it in one corner and leave the rest of your code unchanged. +> +> Quickstart: [link] + +### How the draft applies its guidance + +Every choice above is a rule from elsewhere in the section, and naming them shows how to reproduce the result rather than the wording. The moves, in reading order: + +- **The title is a concrete capability, not the paradigm name.** "Reusable, swappable trait implementations for Rust" is the shortlist hook from [tag-lines.md](tag-lines.md), chosen over "context-generic programming" because the title is the whole pitch for most of this audience, per the [launch-post playbook](formats.md). +- **It opens on a pain, not the paradigm.** The mock-in-tests swap is the most universal problem CGP removes and the entry to lead with for the working developer, drawn straight from [problems-solved.md](problems-solved.md) and keyed to that [reader profile](reader-profiles.md). +- **It shows the workarounds the reader already writes.** Naming `Box`, the threaded generic, and the DI crate lets the CGP version arrive as relief rather than as a new thing to learn — the before/after discipline of [problems-solved.md](problems-solved.md). +- **It concedes the cost and the boundary in plain words.** "This is more than a single trait needs … CGP would be over-engineering" defuses the "why not just traits" and "over-engineered" reflexes from [skepticism.md](skepticism.md) by drawing the line [positioning.md](positioning.md) draws — and the concession is what makes the rest believable to this audience. +- **It states the zero runtime cost precisely.** "Resolved at compile time … no runtime container, no reflection, nothing left in the binary" is the [zero-runtime-cost selling point](selling-points.md), worded to avoid implying any runtime component, per the [vocabulary](vocabulary.md) avoid-list. +- **It calls the wiring "one greppable line."** That answers the "which code actually runs" traceability worry from [skepticism.md](skepticism.md) before it is raised. +- **It closes on stable Rust and gradual adoption.** "A library on stable Rust … a superset of ordinary traits" pairs two selling points that lower the evaluator's adoption risk ([selling-points.md](selling-points.md), the "Still Ordinary Rust" [key feature](key-features.md)). +- **The call to action is the quickstart, not the paradigm.** The skimmer is ready only for a low-commitment look, so the [conversion ladder](formats.md) asks for that and nothing heavier. + +It is as notable for what it avoids: no paradigm name in the opener, no "magic," no "automatically resolves," and no unqualified "DI framework" — each an entry on the [vocabulary](vocabulary.md) avoid-list because each invites a dismissal. + +## The README above the fold + +The README's first screen is where an evaluator and a skimmer both decide in seconds whether to continue, so it must carry the tag line, the headline features, and runnable code before the reader scrolls ([formats.md](formats.md)). The draft models only the part above the fold — the layout term the [glossary](glossary.md) defines — because that is where the decision is made. + +### The draft + +> **Context-Generic Programming** +> +> *A Rust language extension for reusable, swappable trait implementations — checked at compile time, with zero runtime cost.* +> +> A library on stable Rust — no nightly, no fork. `cargo add cgp` +> +> --- +> +> **One Interface, Many Implementations.** Write many interchangeable implementations of the same interface and choose between them per context, with the overlapping and orphan implementations Rust normally forbids made safe because every choice is explicit and local. +> +> **Zero-Cost Abstraction.** Everything is resolved at compile time and compiles down to direct calls, so the flexibility costs nothing at runtime and unused providers never reach the binary. +> +> **Still Ordinary Rust.** A superset of ordinary traits you adopt one piece at a time: providers read like normal impls and implicit arguments like normal parameters, so a codebase can use it in one corner and stay otherwise vanilla. +> +> *(The full headline set is in [key-features.md](key-features.md).)* + +```rust +use cgp::prelude::*; + +#[cgp_component(Greeter)] +pub trait CanGreet { + fn greet(&self) -> String; +} + +#[cgp_auto_getter] +pub trait HasName { + fn name(&self) -> &str; +} + +#[cgp_impl(new GreetHello)] +#[uses(HasName)] +impl Greeter { + fn greet(&self) -> String { + format!("Hello, {}!", self.name()) + } +} + +#[derive(HasField)] +pub struct Person { + pub name: String, +} + +delegate_components! { + Person { + GreeterComponent: GreetHello, + } +} + +fn main() { + let person = Person { name: "World".to_owned() }; + println!("{}", person.greet()); // Hello, World! +} +``` + +> **[Quickstart →]** · **[Is it ready for production? — honest maturity notes →]** + +### How the draft applies its guidance + +The README makes different promises to two readers at once, and each element serves one or both. The moves: + +- **The tag line is layered.** The owned name, a plain descriptor, and a subline that heads off the runtime-container and new-language misreadings are the layered form from [tag-lines.md](tag-lines.md); the subline does the skeptic's work by saying "checked at compile time, zero runtime cost" before the reader supplies a worse reading. +- **It states "a library on stable Rust" and the install line early.** The [evaluator](reader-profiles.md) is scanning for the toolchain gamble, so the reassurance and `cargo add cgp` come before the prose, per [formats.md](formats.md) and the stable-Rust [selling point](selling-points.md). +- **The feature set is the ruthless few, and each title avoids a repellent word.** Three of the canonical headline features from [key-features.md](key-features.md) appear, each title leading with a concrete capability or a recognized Rust term rather than "modular," "macros," or "magic," and each sentence carrying its honest qualifier ("at compile time," "made safe because … explicit and local"). +- **Runnable code sits above the fold.** A reader wants to see the code before the prose, and CGP's own launch feedback asked for exactly a runnable example ([attention-and-engagement.md](attention-and-engagement.md)); the Hello World shows a component, a provider, a getter, and wiring in the smallest honest form. +- **There are two calls to action, matched to the two readers.** The quickstart is the skimmer's rung and the maturity discussion is the evaluator's, straight from the [conversion ladder](formats.md). + +It also leaves out, deliberately, any feature titled "Dependency Injection": on a general front page that label imports the runtime-framework baggage catalogued in [skepticism.md](skepticism.md), so the DI value ships through "Type-Safe Wiring" and "Abstract Over Every Dependency" instead, as [key-features.md](key-features.md) argues. + +## The social thread + +A thread on Mastodon, Bluesky, or X has seconds of attention and one job — earn the click without inviting the dunk — so it leads with a single concrete pain, keeps jargon out of the first post, and lets one link carry the depth ([formats.md](formats.md)). The draft is three posts. + +### The draft + +> **1/** Swapping a real implementation for a mock in Rust usually means a `Box` you pay for at runtime, or a generic type parameter you thread through every layer of your code. There's a third option that costs neither. 🧵 + +> **2/** Define the capability once, write as many implementations as you like, and let each context — your app, your test harness — pick one in a single line. The call site never changes, and it compiles down to a direct call. No trait objects, no runtime dispatch. + +> **3/** It's a library on stable Rust and a superset of ordinary traits, so you can try it in one corner without rewriting anything. And if a dependency has just one implementation, keep using a plain trait — this earns its keep once the implementations multiply. [link] + +### How the draft applies its guidance + +A thread fails in two opposite ways — too jargon-heavy to parse, or so compressed it reads as an overclaim — and the draft is shaped to avoid both. The moves: + +- **The first post is one concrete pain, with no jargon and no paradigm name.** It opens on the mock swap from [problems-solved.md](problems-solved.md), because the [first-contact skimmer](reader-profiles.md) gives the piece three seconds and pattern-matches anything abstract to a category they already dismiss. +- **It earns the click without inviting the dunk.** Conceding "keep using a plain trait" in the last post is the [positioning.md](positioning.md) move that disarms the "over-engineered" reflex ([skepticism.md](skepticism.md)); a thread that only sells is the one a technical crowd piles onto. +- **The mechanism is stated precisely, not hyped.** "Compiles down to a direct call" is the [vocabulary](vocabulary.md)-approved phrasing, chosen over "blazingly fast," which would read as the exact overclaim the skimmer broadcasts. +- **One link carries the depth, and it is the whole call to action.** A thread is a hook-delivery mechanism, not a place to explain, per [formats.md](formats.md). + +## Adapting these to your own piece + +These three are templates for the moves, not for the words, so adapting one to a real piece means re-running the decisions rather than find-and-replacing the copy. Start by naming the one or two dominant [reader profiles](reader-profiles.md) for your channel, then swap the anchor pain for the one that reader feels most sharply — the [orphan-rule escape](problems-solved.md) for the trait-heavy developer, the [error-type swap](problems-solved.md) for the systems reader, and so on — because the pain, not the tag line, is what decides whether the piece lands. + +Two things must survive every adaptation. Keep the conceded cost: it is load-bearing, not optional politeness, and dropping it is how a piece that earned attention loses trust ([skepticism.md](skepticism.md), [positioning.md](positioning.md)). And re-verify every CGP claim and code snippet against the source and the `/cgp` skill before publishing, since a draft copied without that check propagates whatever has gone stale in it. When you find a move these three drafts do not cover, the right next step is a new draft here — a talk opener, a comparison table, a `This Week in Rust` blurb — added in the same shape, so this document grows into the section's library of worked models rather than a fixed set of three. diff --git a/docs/errors/README.md b/docs/errors/README.md index 14efa6e4..4d6e5a6f 100644 --- a/docs/errors/README.md +++ b/docs/errors/README.md @@ -1,6 +1,6 @@ # CGP Error Catalog -This directory is a catalog of the compiler errors a programmer encounters when using CGP, organized by the *kind* of error rather than by the macro that produced it. Each document describes one class of failure: the mistake that triggers it, the shape of the diagnostic the Rust compiler prints, whether that diagnostic contains the real root cause, and — when it does — where in the output to find it. The catalog is the fifth section of the knowledge base, alongside [reference/](../reference/README.md), [concepts/](../concepts/README.md), [examples/](../examples/README.md), and [guides/](../guides/README.md). +This directory is a catalog of the compiler errors a programmer encounters when using CGP, organized by the *kind* of error rather than by the macro that produced it. Each document describes one class of failure: the mistake that triggers it, the shape of the diagnostic the Rust compiler prints, whether that diagnostic contains the real root cause, and — when it does — where in the output to find it. The catalog is one of the knowledge base's top-level sections, alongside [reference/](../reference/README.md), [concepts/](../concepts/README.md), [examples/](../examples/README.md), [guides/](../guides/README.md), and [related-work/](../related-work/README.md). ## Why this exists diff --git a/docs/reference/README.md b/docs/reference/README.md index c9e97fea..c5317a42 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -2,6 +2,54 @@ This directory documents every CGP construct — one self-contained document per construct, each explaining its purpose, syntax or definition, expansion or behavior, examples, related constructs, and source. The documents are written for agents who need precise per-construct semantics, and they point only at library source, never at a test. The high-level conceptual framing that connects the constructs lives in the sibling [concepts/](../concepts/README.md) directory; the internal mechanics of each macro — its pipeline, the functions that synthesize its output, and every pointer into the test suite — live in the sibling [implementation/](../implementation/README.md) directory, to which each reference document's Source section links; the `/cgp` skill remains a complementary teaching aid. The authoring rules, document template, and the requirement to keep these documents in sync with the code live in [../AGENTS.md](../AGENTS.md). +## Summary + +This section summarizes every documented construct, grouped by the job it does and ordered from the constructs almost every CGP task needs down to the specialized and reference-only ones. Each group says when to reach for it and links to the per-construct document that carries the full grammar, expansion, and examples. The summary tells you only what a construct is *for*, not how it behaves in every corner, so read the linked document before writing, changing, reviewing, or debugging code that uses the construct — that document is the ground truth, and this catalog exists to route you to the right one. + +### Core constructs + +These are the constructs behind almost every CGP program: defining a component, writing a provider for it, and wiring a context to the provider it uses. [`#[cgp_component]`](macros/cgp_component.md) turns one trait into a component — the consumer trait callers invoke, the provider trait implementers target, and the blanket impls plus component-name marker that connect them. A provider is then written with [`#[cgp_impl]`](macros/cgp_impl.md), the idiomatic form that keeps `self`/`Self` and consumer-trait signatures and desugars into the inside-out provider-trait shape; its lower-level layers [`#[cgp_provider]`](macros/cgp_provider.md) and [`#[cgp_new_provider]`](macros/cgp_new_provider.md) implement the provider trait directly (the latter also declaring the provider struct) and are mostly what you read rather than write. When a capability has a single implementation and needs no wiring, [`#[cgp_fn]`](macros/cgp_fn.md) generates a blanket-impl trait straight from a function, and [`#[blanket_trait]`](macros/blanket_trait.md) does the same from a trait with default methods and supertrait dependencies; [`#[async_trait]`](macros/async_trait.md) rewrites a trait's `async fn` declarations into the lint-clean `-> impl Future` form that CGP's async methods use. + +Wiring is where a concrete context chooses its providers. [`delegate_components!`](macros/delegate_components.md) builds a context's type-level table mapping each component to a provider — read its document for the array-key, generic-list, aggregate-provider (`new`), and per-type `open` forms — and that table is the [`DelegateComponent`](traits/delegate_component.md) trait underneath, a compile-time key→value map that both ordinary wiring and inner dispatch tables share. Two providers appear directly in wiring: [`UseContext`](providers/use_context.md) satisfies a provider trait by routing back through the context's own consumer-trait impl (and is the default inner provider of a higher-order provider), and [`UseDefault`](providers/use_default.md) selects a component's default method bodies. To dispatch one component per value of a generic parameter, prefer the `open` statement of `delegate_components!`; the legacy path is the [`UseDelegate`](providers/use_delegate.md) provider and the [`#[derive_delegate]`](attributes/derive_delegate.md) attribute that generates its dispatch impl, which you still meet in existing code and in CGP's own error and handler components. + +### Basic field access + +Reach for these whenever a provider needs a value out of its context — the most common form of dependency injection in CGP. [`#[derive(HasField)]`](derives/derive_has_field.md) gives a struct per-field getters keyed by a type-level tag, implementing the [`HasField`](traits/has_field.md) trait (and its mutable and provider-side mirrors) that all value injection stands on; the tags are [`Symbol!`](macros/symbol.md) type-level strings for named fields and [`Index`](types/index.md) type-level numbers for tuple fields. The default way to read such a field is an [`#[implicit]`](attributes/implicit.md) argument, which looks like an ordinary function parameter but is sourced from a same-named context field, and whose access rules (`.clone()` for owned, `.as_str()` for `&str`, and the option/slice/mutable forms) live in its document. + +A getter trait is the sparing alternative for the cases an implicit argument cannot reach — a field on another type, a named shared capability, or a getter carrying an inferred associated type. [`#[cgp_auto_getter]`](macros/cgp_auto_getter.md) generates a blanket getter impl over `HasField` keyed by the method name, while the advanced [`#[cgp_getter]`](macros/cgp_getter.md) makes the getter a full component so its source field is chosen at wiring time through the [`UseField`](providers/use_field.md) provider — with [`UseFieldRef`](providers/use_field_ref.md) for `AsRef`/`AsMut` access and [`UseFields`](providers/use_fields.md) for the method-name convention. [`ChainGetters`](providers/chain_getters.md) composes getters to reach a field on a nested context, and [`MRef`](types/mref.md) is the owned-or-borrowed getter return type for a getter that may lend or produce its value. + +### Imports + +These attributes declare a provider's dependencies as if importing them, keeping the constraints off the public trait interface. [`#[uses]`](attributes/uses.md) adds `Self:` capability bounds that read like a `use` statement — the idiomatic replacement for a hand-written `where Self: Trait` clause — and [`#[use_provider]`](attributes/use_provider.md) completes an inner provider's bound in a higher-order provider by filling in the stray `` context argument the provider trait requires. [`#[extend]`](attributes/extend.md) adds a supertrait to the generated trait (the `pub use` counterpart of `#[uses]`, and the only way to add a supertrait in `#[cgp_fn]`), while [`#[extend_where]`](attributes/extend_where.md) promotes a `where` predicate onto a `#[cgp_fn]` trait's own definition rather than only its impl. The `#[impl_generics(...)]` attribute, which adds a bounded generic parameter to a `#[cgp_fn]` impl alone, is documented within [`#[cgp_fn]`](macros/cgp_fn.md). + +### Abstract types + +Use these when generic code must name a type — an error type, a scalar, a runtime — that each context chooses for itself. [`#[cgp_type]`](macros/cgp_type.md) defines an abstract-type component from a trait with one associated type, layering a [`UseType`](providers/use_type.md) blanket impl on top of [`#[cgp_component]`](macros/cgp_component.md) so a context binds the concrete type by wiring the component to `UseType`; that machinery rests on CGP's single built-in abstract-type component, [`HasType` / `TypeProvider`](components/has_type.md). The [`#[use_type]`](attributes/use_type.md) attribute — distinct from the `UseType` provider despite the shared name — imports an abstract type into a `#[cgp_fn]`/`#[cgp_impl]`/`#[cgp_component]` definition, rewriting a bare `Error` or `Scalar` into its fully-qualified `::Type` form and adding the supertrait or bound, and it also carries the equality form that pins an abstract type to a concrete one. [`UseDelegatedType`](providers/use_delegated_type.md) resolves an abstract type through a lookup table instead of a fixed type, and [`WithProvider`](providers/with_provider.md) (with its `WithType`/`WithField`/`WithContext`/`WithDelegatedType` aliases) is the adapter that lets a foundational `TypeProvider` or `FieldGetter` stand in as a named component's provider. + +### Error handling + +CGP makes the error type abstract so fallible generic code never names a concrete error, and these components carry that strategy. [`HasErrorType`](components/has_error_type.md) gives a context one shared `Error` type (an abstract-type component, so wired with `UseType`), and [`CanRaiseError` / `CanWrapError`](components/can_raise_error.md) construct that error from a source error and attach detail to it, dispatching per source or detail type. The interchangeable strategies that satisfy them — the [error providers](providers/error_providers.md) `RaiseFrom`, `ReturnError`, `RaiseInfallible`, `DebugError`/`DisplayError`, `DiscardDetail`, and `PanicOnError` — stay generic over the context's error type, while the concrete backends (`cgp-error-anyhow`, `cgp-error-eyre`, `cgp-error-std`) are opt-in and named in their own crates. The wiring keys and backend providers are deliberately not in the prelude and must be imported from `cgp::core::error` / `cgp::extra::error`. + +### Checks and debugging + +CGP wiring is lazy, so a context can compile while wired wrong; these constructs force that failure to surface, readably, at the wiring site. [`check_components!`](macros/check_components.md) asserts at compile time that a context can use each listed component, and [`delegate_and_check_components!`](macros/delegate_and_check_components.md) fuses wiring and checking for basic contexts (but never for an aggregate provider). Both build on two marker traits: [`CanUseComponent`](traits/can_use_component.md), the context-side check that a context both delegates a component and its provider's dependencies hold, and [`IsProviderFor`](traits/is_provider_for.md), the supertrait every provider trait carries that re-exposes a provider's `where` bounds so the compiler names the actual missing dependency instead of a bare "trait not implemented". The `#[check_providers(...)]` form of `check_components!` asserts `IsProviderFor` on each provider directly, which is how a broken layer of a higher-order provider stack is localized. + +### Namespaces + +Namespaces are reusable, inheritable wiring tables — CGP's preset mechanism — for keeping top-level wiring short as component counts grow. [`cgp_namespace!`](macros/cgp_namespace.md) defines a namespace (optionally inheriting a parent); a context joins it with a `namespace` header inside [`delegate_components!`](macros/delegate_components.md), a component registers into one with the `#[prefix(...)]` attribute, and a provider registers as a per-type default with `#[default_impl(...)]` — the last two documented within `cgp_namespace!` and [`DefaultNamespace`](traits/default_namespace.md). The mechanism underneath is the [`RedirectLookup`](providers/redirect_lookup.md) provider, which re-routes a lookup along a type-level [`Path!`](macros/path.md) / [`PathCons`](types/path_cons.md), together with the [`DefaultNamespace` / `DefaultImpls`](traits/default_namespace.md) traits that resolve inherited and per-type defaults. The lightweight `open` statement of `delegate_components!` is a special case of the same redirection for per-type dispatch wired directly on one context. + +### Handlers and computation + +This family models computation as swappable components along the axes of sync/async, fallible/infallible, and input/input-free — reach for it for pipelines, I/O, and type-level interpreters. [`Computer`](components/computer.md) is the pure synchronous transform (with by-reference and async variants), [`TryComputer`](components/try_computer.md) adds fallibility, [`Handler`](components/handler.md) is the general async-and-fallible workhorse, and [`Producer`](components/producer.md) is the input-free case; [`CanRun` / `CanSendRun`](components/runner.md) run tasks and [`HasRuntime` / `HasRuntimeType`](components/has_runtime.md) supply the abstract runtime they execute against. A provider is written from a plain function with [`#[cgp_computer]`](macros/cgp_computer.md) or [`#[cgp_producer]`](macros/cgp_producer.md), which also wire the promotion tables that let one function answer the whole family, and [`#[cgp_auto_dispatch]`](macros/cgp_auto_dispatch.md) generates a variant-dispatching handler from a per-type trait. The providers that build and route handlers are the [handler combinators](providers/handler_combinators.md) (`ComposeHandlers`, `PipeHandlers`, `ReturnInput`, the `Promote*` lifts, and `UseInputDelegate`), the [dispatch combinators](providers/dispatch_combinators.md) that match enum variants and assemble records, and the [monad providers](providers/monad_providers.md) (`PipeMonadic` with the ident/ok/err monads) built on the [monad traits](traits/monad.md) for short-circuiting pipelines. + +### Extensible data types + +These derives and traits let generic code build and read structs and enums by their named fields and variants without naming the concrete type. [`#[derive(CgpData)]`](derives/derive_cgp_data.md) is the umbrella derive; its struct- and enum-specific faces are [`#[derive(CgpRecord)]`](derives/derive_cgp_record.md) and [`#[derive(CgpVariant)]`](derives/derive_cgp_variant.md), and its individual slices are [`#[derive(HasFields)]`](derives/derive_has_fields.md) (the whole-shape view), [`#[derive(BuildField)]`](derives/derive_build_field.md) (the record builder), [`#[derive(ExtractField)]`](derives/derive_extract_field.md) (the variant extractor), and [`#[derive(FromVariant)]`](derives/derive_from_variant.md) (variant construction). The traits behind them are [`HasFields`](traits/has_fields.md), the incremental-builder family in [`HasBuilder`](traits/has_builder.md), the extractor family in [`ExtractField`](traits/extract_field.md), [`FromVariant`](traits/from_variant.md), the presence markers of [`MapType`](traits/map_type.md), the list algebra of [`AppendProduct` / `ConcatProduct` / `MapFields`](traits/product_ops.md), the structural casts [`CanUpcast` / `CanDowncast` / `CanBuildFrom`](traits/cast.md), and the [optional-field extensions](traits/optional_fields.md) for defaulted and optional fields. Each entry in the shape is a [`Field`](types/field.md) pairing a value with its type-level name tag. + +### Type-level primitives + +These are the type-level building blocks the rest of CGP is constructed from — mostly written through sugar, and otherwise needing only to be recognized in expansions and error messages. The construction macros are [`Symbol!`](macros/symbol.md) (a type-level string, for field names), [`Product!`](macros/product.md) and [`Sum!`](macros/sum.md) (type-level record and variant lists), and [`Path!`](macros/path.md) (a routing path); their expanded spines are [`Cons` / `Nil`](types/cons.md) for products, [`Either` / `Void`](types/either.md) for sums, [`Chars`](types/chars.md) for the string behind `Symbol`, and [`PathCons`](types/path_cons.md) for paths. Two further lifts make non-type things addressable in trait resolution — [`Index`](types/index.md) for a tuple-field position and [`Life`](types/life.md) for a lifetime — and [`MRef`](types/mref.md) is the owned-or-borrowed getter value. The [`StaticFormat` / `StaticString` / `ConcatPath`](traits/static_format.md) traits recover these type-level strings and paths back into runtime data. + ## Directory layout The documents are grouped into subdirectories by the *kind* of construct, so a reader looking for "the macro I invoke", "the trait the macro generates", or "the provider I wire" each has an obvious place to start. A new document goes in the subdirectory that matches what the construct is; when you add one, place it accordingly and register it in the matching section below. The high-level conceptual overviews that tie multiple constructs together — the consumer/provider duality, dependency injection, namespaces, handlers, and so on — live in the sibling [concepts/](../concepts/README.md) directory rather than here, each pointing into these per-construct documents for the mechanics. diff --git a/docs/related-work/AGENTS.md b/docs/related-work/AGENTS.md new file mode 100644 index 00000000..a1a62625 --- /dev/null +++ b/docs/related-work/AGENTS.md @@ -0,0 +1,51 @@ +# AGENTS.md + +This file provides guidance to LLM agents when working with code in this repository. + +This directory holds the **related-work** documents of the CGP knowledge base — one document per external concept, framework, or language feature that solves a problem CGP also solves, or that resembles a CGP construct closely enough that a reader coming from it can be met on familiar ground. Read the knowledge-base [README.md](../README.md) for the background on the whole base, and the governing [../AGENTS.md](../AGENTS.md) for the rules every section shares. The rules below are specific to related work. + +## Why this section exists + +A related-work document exists to serve *future user-facing documentation*, not to teach CGP directly. The rest of the knowledge base explains CGP on its own terms; this section records how a mainstream idea — dependency injection, implicit parameters, type classes, and so on — actually works, what its users value and resent about it, and where CGP lands relative to it. An agent later asked to write a tutorial, a blog post, or a landing page *for readers who already know that idea* reads the matching related-work document first, then leans on the reader's existing intuition to make the CGP explanation land. The audience of the eventual writing is a practitioner of the related concept; the audience of the document itself is the agent preparing to address them. + +This purpose sets the bar for the content. A related-work document is worth writing only if it captures the concept faithfully enough that an agent could explain it to that concept's own community without embarrassment, and honestly enough that the comparison to CGP survives a skeptic who prefers the other tool. Shallow praise of CGP and strawman versions of the related work both defeat the point: the reader we are ultimately writing for will spot either one immediately. + +## What every related-work document must cover + +Each document explains one related concept in genuine depth and then positions CGP against it. The explanation comes first and stands on its own — a reader learns the related work here, not just a caricature of it — and the CGP comparison follows once the concept is on the table. Every document works through the same obligations: + +- **Explain the concept in real detail.** Cover the important sub-concepts of the related work, not just its headline. Define its vocabulary, show how it is actually used, and explain the mechanism behind it, at the depth a practitioner of it would recognize as correct. +- **Show it in code.** Demonstrate each important sub-concept with a code snippet in the related work's own language and idiom, and — where CGP has a counterpart — show the same thing written in CGP right beside it, so the two can be read against each other. Not every snippet has a CGP equivalent; say so when it does not. +- **Give both the pros and the cons.** State plainly what users *like* about the concept and what they *dislike* — the pain points, the footguns, the recurring complaints — drawing on real community sentiment rather than invented objections. A document that lists only weaknesses is as useless as one that lists only strengths. +- **Compare how CGP solves the problem differently.** Explain where CGP takes the same approach, where it diverges, and *why* — what CGP's design buys and what it costs relative to the related work. Be fair: name the cases where the related work is the better fit. +- **Analyze how to present CGP positively to someone who knows the concept.** Close with concrete guidance for the future writer: which of the reader's existing intuitions to build on, which analogies land and which mislead, which CGP advantages will resonate with this particular audience, and which of their expectations CGP will violate and must therefore address head-on. This positioning analysis is the section the rest of the document exists to support. + +## Sourcing and citations + +**Unlike the [examples](../examples/) directory, related-work documents cite their sources.** An example is re-derived as native knowledge-base material with no pointer to where the idea came from; a related-work document is the opposite — its credibility rests on being a faithful account of an external thing, so it must be traceable to that thing. Research the concept from its authoritative documentation, primary references, and representative community discussion before writing, and record what you used. + +Study the research literature directly when the concept comes out of programming-language research, as row polymorphism, type classes, effect systems, and their kin do. For these ideas the online resources to study are not only the language documentation that popularized them but the academic papers and the serious technical articles that define and analyze them: read the foundational paper a feature descends from and, where it exists, the current research that generalizes or corrects it, alongside the blog posts and talks that make the theory legible. The primary literature is where a PL concept's precise definition, formal properties, and design space actually live — often stated nowhere else — so treat it as a first-class source to study and to cite, not an optional supplement to the documentation. + +Ground every factual claim about the related work in a real source. Cite official language or framework documentation for how a feature behaves, primary papers for a concept's origin or formal properties, and reputable community writing for sentiment about what users like or dislike — attributing opinion as opinion, not as fact. Collect the citations in a **Sources** section at the end of the document, as a framed list of links with a short note on what each supports, and reference them inline where a specific claim leans on one. Do not invent quotations, statistics, or version-specific behavior; when unsure whether a detail is current, verify it against the source rather than trusting memory. + +Keep the account current and neutral. Describe the related work as it exists now, note the version or edition when behavior is version-specific (Scala 2 `implicit` versus Scala 3 `given`/`using`, for instance), and represent the concept as its proponents would recognize it before critiquing it. + +## The CGP side must obey the synchronization rule + +Every CGP snippet in a related-work document is bound by the [synchronization rule](../AGENTS.md) exactly as a reference document's Expansion section is. A CGP comparison that shows syntax the macros no longer accept, or an expansion the code no longer produces, is a bug in the change that made it stale — and a especially damaging one here, because it will be quoted into user-facing material and shown to the very audience most likely to scrutinize it. Verify each CGP snippet against the source and the current macro behavior, invoke the `/cgp` skill before writing any CGP code, and prefer the modern idioms the skill and the [guides](../guides/) recommend. Draw CGP snippets from the [examples](../examples/) and the running scenarios the rest of the base already uses, rather than inventing fresh contexts, so the CGP side of every comparison speaks the knowledge base's shared vocabulary. + +## Document structure + +Each related-work document follows the same shape so readers can navigate any of them by habit. Open with a level-one heading naming the concept and a one-sentence summary of what it is and why it is worth comparing to CGP. Then proceed through these sections, using the same headings: + +- **Purpose** — what problem the concept solves, and why a CGP reader should care that it exists. One or two paragraphs of framing. +- **The concept in depth** — the faithful explanation of the related work: its sub-concepts, its vocabulary, and its mechanism, each illustrated with a code snippet in the related work's own language. This is the section that must satisfy a practitioner of the concept. Use titled subsections when the concept has several distinct parts. +- **How CGP expresses it** — the same problems written in CGP, shown against the related-work snippets above, with prose explaining where the two align and where they part ways. +- **What users like and dislike** — the honest pros and cons of the related work, drawn from real sentiment and cited. +- **How CGP compares** — the design-level comparison: what CGP's approach buys, what it costs, and where the related work remains the better choice. +- **Presenting CGP to someone who knows this** — the positioning guidance for future user-facing writing: intuitions to build on, analogies that land or mislead, advantages that resonate, and expectations to address before they trip the reader. +- **Sources** — the framed list of citations described above. + +Follow the dual-reader prose style (the `/dual-reader-prose` skill) throughout: open every section with a self-contained topic sentence, frame every list, and let the prose carry the meaning around each code block. Prefer plain language and the knowledge base's established CGP vocabulary — consumer trait, provider trait, provider, wiring, impl-side dependency, context — so a reader moving between this section and the rest never reconciles two dialects. + +Register every new document in the catalog in [README.md](README.md) in the same change that adds it, and cross-link generously: to the [concepts](../concepts/README.md) for the CGP idea a comparison rests on, to the [reference](../reference/README.md) for the exact syntax of any construct shown, and to sibling related-work documents when two concepts are themselves related. diff --git a/docs/related-work/README.md b/docs/related-work/README.md new file mode 100644 index 00000000..036618a5 --- /dev/null +++ b/docs/related-work/README.md @@ -0,0 +1,28 @@ +# CGP Related Work + +This directory compares CGP to the outside ideas it resembles — one document per external concept, framework, or language feature that solves a problem CGP also solves, or that a reader might already know well enough to learn CGP through. Each document explains the related work faithfully and in depth, weighs what its users like and dislike about it, and then positions CGP against it. The documents exist to make CGP legible to readers who arrive with an existing mental model, by meeting that model on its own terms before showing where CGP diverges. + +## Why this exists + +Related-work documents serve *future user-facing documentation*, not the reader trying to learn CGP directly. When an agent later writes a tutorial, an article, or a landing page aimed at people who already know dependency injection or implicit parameters, it reads the matching document here first, then builds the CGP explanation on the intuitions those readers already hold. The eventual audience is a practitioner of the related concept; the audience of the document itself is the agent preparing to address them, and its job is to supply the honest comparison and the positioning strategy that writing will rest on. + +Honesty is the whole value of the section. A document that flatters CGP or strawmans the related work is worse than useless, because the readers it ultimately serves are the people most able to see through both. So each document explains the related work well enough that its own community would recognize the account, states the genuine pros and cons of both sides, and names the cases where the related work is simply the better tool. The [AGENTS.md](AGENTS.md) in this directory carries the authoring rules — chiefly the obligations every document must meet, and the one rule that sets this section apart from the rest of the knowledge base: unlike examples, related-work documents cite their sources. + +## How related work differs from the other sections + +Related work answers a question none of the other sections do: *how does CGP relate to the idea I already know?* A [reference document](../reference/README.md) explains one CGP construct completely; a [concept document](../concepts/README.md) explains one cross-cutting CGP idea; an [example](../examples/README.md) develops one use case end to end; a [guide](../guides/README.md) directs a choice between CGP constructs. All four look inward at CGP. A related-work document looks outward — it takes something from outside CGP as the subject, explains it on its own terms, and uses CGP only as the point of comparison. Where an example is re-derived as native material with no pointer to its origin, a related-work document is the opposite: its credibility depends on being a traceable, cited account of the external thing. + +The two therefore lean on the rest of the base rather than restating it. A related-work document links to the [concepts](../concepts/README.md) for the CGP idea a comparison rests on and to the [reference](../reference/README.md) for the exact syntax of any construct it shows, keeping its own focus on the external concept and the comparison. When a CGP snippet is needed, it is drawn from the running scenarios the [examples](../examples/README.md) already use, so the CGP side of every comparison speaks the knowledge base's shared vocabulary. + +## The catalog + +Each document below names an external concept and compares it to CGP; the authoring rules for adding one live in [AGENTS.md](AGENTS.md). + +- [Dependency injection](dependency-injection.md) — the IoC-container and constructor-injection model of Spring, Guice, Dagger, and their kin, and how CGP's impl-side dependencies and per-context wiring inject dependencies without a container, reflection, or runtime graph. +- [Implicit parameters](implicit-parameters.md) — the implicitly-passed context arguments of Scala's `given`/`using` and Haskell's `ImplicitParams`, alongside the type-class resolution both build on, and how CGP's implicit arguments and context-threaded wiring achieve the same context propagation while trading global coherence for per-context choice. +- [Row polymorphism, structural typing, and extensible data types](row-polymorphism.md) — the field-and-variant-shape–driven typing of PureScript's rows, OCaml's polymorphic variants, and the languages around them, framed by Morris and McKinna's "rows by any other name" row-theory account, and how CGP brings the same extensible records and variants to nominal Rust through derived type-level shapes rather than a built-in row kind. +- [Algebraic effects and handlers](algebraic-effects.md) — the operations-and-handlers model of Koka, OCaml, Flix, and Eff, where a handler interprets an effect by capturing the continuation and resuming it zero, one, or many times, and how CGP reproduces the effect/handler split but only for the exactly-once fragment that the literature identifies as dynamic binding — resolved statically per context rather than dynamically down a call stack, and extended to configuring abstract types. +- [ML modules and modular implicits](ml-modules.md) — the signatures, structures, and functors of OCaml and Standard ML, and the modular-implicits extension that adds type-directed resolution over them, framed by Dreyer, Harper, and Chakravarty's "modular type classes," and how CGP maps components to signatures, providers to structures, and higher-order providers to functors while replacing manual, ordered functor application with a declarative `delegate_components!` table resolved per context. +- [Type classes](type-classes.md) — the principled ad-hoc polymorphism of Haskell, Agda, and Lean, dictionary-passing and coherence, the overlapping- and incoherent-instance extensions that strain against it, the diamond problem in dependently-typed settings, and Dreyer, Harper, and Chakravarty's "modular type classes," and how CGP is a type-class system that removes global coherence and replaces implicit resolution with explicit per-context selection — making overlapping and incoherent instances ordinary and deterministic rather than exceptional and dangerous. +- [Dynamic dispatch, dynamic typing, and prototypal inheritance](dynamic-dispatch.md) — the runtime mechanisms of dynamically-typed and object-oriented languages: late binding and message passing, the vtables and method dictionaries that implement them, duck typing, and prototype-based delegation in Self, JavaScript, and Lua, and how CGP reproduces their openness — many implementations behind one interface, behavior shared by delegation with `self` staying bound, defaults inherited and overridden, code that reads as if it just sends messages — while resolving all of it statically, so its vtable is a type-level table erased before runtime and its prototype chain is walked by the compiler. +- [Reflection and compile-time introspection](reflection.md) — full treatments of Bevy's runtime reflection, Zig's `comptime` as the compile-time model, and Rust's nightly reflection MVP (tracked from its source, tracking issues, and PRs: `core::mem::type_info`, the `TypeKind`/`Struct`/`Field` API, the `#[rustc_comptime]` restriction, and the open road to an RFC), set against Go, Java, C++26, D, and facet, and how CGP reaches the same generic-over-structure payoff by encoding a type's shape as type-level lists the trait system resolves against — shown through `cgp-serde`'s `SerializeFields` and compared with facet and the reflection MVP, carrying field types as types so it recurses into them, checked when written, and extended to behavior and abstract-type selection. diff --git a/docs/related-work/algebraic-effects.md b/docs/related-work/algebraic-effects.md new file mode 100644 index 00000000..f19a25b9 --- /dev/null +++ b/docs/related-work/algebraic-effects.md @@ -0,0 +1,209 @@ +# Algebraic effects and handlers + +Algebraic effects and handlers are a way to define side-effecting *operations* — throwing, reading state, yielding, awaiting — separately from the *handlers* that interpret them, so that one piece of effectful code can be run under many different interpretations, with a handler free to capture the rest of the computation as a continuation and resume it zero, one, or many times. CGP shares the split between an operation and its interpretation, and even the idea of choosing the interpretation from the surrounding context, but it keeps only the fragment where the continuation is used exactly once and in place — which turns out to be dynamic binding, resolved statically per context rather than dynamically down a call stack. + +## Purpose + +Algebraic effects solve the problem of writing code that *does* something effectful without fixing *how* that effect is carried out. A function that needs to read a configuration value, log a message, throw an error, suspend for I/O, or make a nondeterministic choice normally has to commit to a concrete mechanism — a global, a `Result`, a monad, a callback — and that commitment leaks into its type and forces every caller to accommodate it. Algebraic effects let the function instead *perform an operation* named by an abstract effect, and defer the meaning of that operation to a *handler* installed somewhere up the call stack. The same code runs against a real logger in production, a collector in a test, and a no-op in a benchmark, with only the handler changing. + +This is the same separation CGP draws between a [consumer trait and its providers](../concepts/consumer-and-provider-traits.md), which is why the comparison is worth drawing carefully rather than casually. Both paradigms take a capability, expose it as an interface a caller invokes without naming an implementation, and supply the implementation from the surroundings. Where they diverge is *what a handler is allowed to do* and *how the surroundings choose it*: an effect handler receives the continuation and wields real control-flow power, chosen by dynamic scope; a CGP provider is an ordinary function selected by the context's type at compile time. Showing a reader where that line falls — and why CGP sits on the side of it that coincides with dynamic binding — is the heart of this comparison, and it draws on the [row-polymorphism](row-polymorphism.md) account too, since the effect systems that type these operations do so with the very row types that document covers. + +## The concept in depth + +Algebraic effects come in layers that a reader should keep distinct: the *operations* that name an effect, the *equational theory* that classically justifies calling it "algebraic," the *handlers* that interpret operations by grabbing the continuation, the *effect system* that types which operations a computation may perform, and the concrete language designs — Koka, OCaml, Flix, Eff — that realize these to different degrees. The subsections below build up in that order, and the final one isolates the single fragment that CGP corresponds to. + +### Operations, effects, and the equational theory + +An *effect* is a signature of *operations*, and a computation produces the effect by *performing* one of them. The founding idea, due to Gordon Plotkin and John Power, is that impure behavior arises from a set of operations — `get` and `put` for mutable state, `read` and `print` for I/O, `raise` for exceptions — rather than from an opaque notion of "side effect" ([Plotkin & Pretnar, *Handling Algebraic Effects*](https://homepages.inf.ed.ac.uk/gdp/publications/handling-algebraic-effects.pdf)). What made the account *algebraic* is that each effect came with an *equational theory*: laws the operations obey, such as the state laws relating `get` and `put`, whose free model induces exactly the monad for that effect ([Plotkin & Pretnar 2013](https://lmcs.episciences.org/705); [Bauer & Pretnar, *Programming with Algebraic Effects and Handlers*](https://www.researchgate.net/publication/221671686_Programming_with_Algebraic_Effects_and_Handlers)). This theoretical grounding is where the name comes from, but it is largely vestigial in the practical languages below, which keep the operations-and-handlers structure and drop the laws — a point worth holding onto, because CGP keeps even less of the algebra than they do. + +### Handlers and the continuation + +A *handler* interprets the operations of an effect, and its defining power is that it receives the *continuation* — the suspended rest of the computation from the point the operation was performed. When a computation performs `op(arg)`, control transfers to the nearest enclosing handler for that operation, which receives both the argument and the continuation as a first-class, delimited resumption ([Pretnar, *An Introduction to Algebraic Effects and Handlers*](https://www.eff-lang.org/handlers-tutorial.pdf)). This generalizes an exception handler in one decisive way: an exception handler can only *abandon* the computation, whereas an effect handler can *resume* it. Plotkin and Pretnar gave this its semantics — a handler is a *model* of the effect's theory, and handling is the unique homomorphism from the free model into it ([Plotkin & Pretnar, *Handlers of Algebraic Effects*](https://homepages.inf.ed.ac.uk/gdp/publications/Effect_Handlers.pdf)). + +How many times a handler invokes the continuation is what determines the effect it realizes, and the three cases are worth naming because they mark exactly where CGP can and cannot follow: + +- **Zero times** — the handler discards the continuation and returns its own value, which is *exception* behavior: `raise` aborts to the handler and never comes back. +- **Once** — the handler resumes the continuation with a result, which is the *ordinary* case: reading state, dynamic binding, logging, a normal function-like call that yields a value and lets the caller carry on. +- **Many times** — the handler resumes the continuation more than once, which is what makes effects *powerful*: nondeterminism and backtracking (try both branches), generators (yield and later resume), cooperative scheduling and async/await (suspend, run something else, resume). This is *multi-shot* resumption, and it is impossible to express as an ordinary function return. + +### Effect typing: rows, sets, or nothing + +An *effect system* tracks, in a computation's type, which effects it may perform, so that unhandled effects can be caught statically — and languages differ sharply in whether they do this. Koka types effects as a *row* — `` in a signature means the function may throw and diverge — building directly on row polymorphism ([Leijen, *Algebraic Effects for Functional Programming*](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/08/algeff-tr-2016-v2.pdf)); this is the same row machinery surveyed in [row polymorphism](row-polymorphism.md), applied to effects instead of records. Flix types effects as a *set* over a lattice and uses the result for *purity reflection*, letting the compiler know when code is pure enough to parallelize or eliminate ([Madsen et al., *Programming with Purity Reflection*](https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.ECOOP.2023.18)). At the other extreme, Eff and OCaml 5 deliberately do *not* track effects in types at all; an unhandled effect is a runtime failure rather than a type error ([OCaml manual, *Effect handlers*](https://ocaml.org/manual/5.5/effects.html)). The presence or absence of this static check is the sharpest axis of variation among the languages, and the one where CGP's `check_components!` has something precise to say. + +### The concept in three languages + +The same effect looks recognizably similar across the languages that implement it, which makes their differences legible. In **Koka**, an effect declares operations that are marked `ctl` (full control, resume any number of times), `fun` (tail-resumptive — resume exactly once, in place), or `val`; the effect appears as a row in the type, and a handler is installed with `with`: + +```koka +effect ask + fun ask() : int // `fun`: tail-resumptive, resumes exactly once + +fun add-twice() : ask int + ask() + ask() + +fun main() : console () + with fun ask() 21 // install a handler that supplies 21 + println( add-twice() ) // 42 +``` + +A `ctl` operation is the multi-shot form — a `ctl flip()` handler may call `resume(True)` *and* `resume(False)`, resuming the continuation twice to explore both branches — and if an effect is never handled, Koka keeps it in the row and the type checker demands the enclosing function declare it, so an unhandled effect is ultimately a *type error*. + +In **OCaml 5**, effects are declared by extending `Effect.t`, performed with `perform`, and handled by matching on the effect and its continuation `k`, which is resumed with `continue`: + +```ocaml +type _ Effect.t += Ask : int Effect.t + +let add_twice () = perform Ask + perform Ask + +let () = + let open Effect.Deep in + try_with add_twice () + { effc = fun (type a) (eff : a Effect.t) -> + match eff with + | Ask -> Some (fun (k : (a, _) continuation) -> continue k 21) + | _ -> None } +``` + +OCaml's continuations are **one-shot**: `k` may be resumed *at most once*, enforced by a dynamic check that raises `Continuation_already_resumed` on a second resume, a restriction chosen because one-shot continuations are far cheaper and suffice for the concurrency schedulers the feature was built for ([Sivaramakrishnan et al., *Retrofitting Effect Handlers onto OCaml*](https://kcsrk.info/slides/handlers_edinburgh.pdf)). OCaml tracks **no** effects in types, so a `perform` with no matching handler raises `Effect.Unhandled` at runtime. + +In **Flix**, an effect is declared with `eff`, appears in a function type after a backslash, and is handled with `run … with handler`: + +```flix +eff Ask { + def ask(): Int32 +} + +def addTwice(): Int32 \ Ask = + Ask.ask() + Ask.ask() + +def main(): Int32 = + run { + addTwice() + } with handler Ask { + def ask(k) = k(21) + } +``` + +Flix's set-based effect system means `\ Ask` is part of `addTwice`'s type, and the handler's continuation `k` makes the resume explicit, just as Koka's `resume` and OCaml's `continue` do. + +### The fragment CGP corresponds to: tail-resumptive handlers *are* dynamic binding + +The single fact that makes the CGP comparison precise is that a handler which resumes the continuation *exactly once, in tail position* is no longer doing anything control-theoretic — it is dynamic binding. A handler is *tail-resumptive* when every operation clause invokes the continuation in tail position, and the literature names its canonical example outright: **the canonical tail-resumptive handler is dynamic binding** ([Xie et al., *Effect Handlers, Evidently*](https://www.microsoft.com/en-us/research/wp-content/uploads/2020/07/evidently-5f0b7dbc1a998.pdf)). The reason this matters for implementation, and for CGP, is that such a handler never needs to capture the continuation at all: the compiler can run the operation *in place* on the current stack, replacing the dynamic search for a handler with a constant-offset lookup into an *evidence vector* of handlers passed down like a dictionary ([Xie & Leijen, *Generalized Evidence Passing for Effect Handlers*](https://xnning.github.io/papers/multip.pdf)). In other words, the efficient compilation of the exactly-once fragment of effect handlers *is* dictionary passing. That is the fragment CGP occupies — and it occupies it directly, with no dynamic search underneath and nothing else on top. + +## How CGP expresses it + +CGP reproduces the operations-and-handlers structure with its consumer/provider split, but every CGP "handler" is an ordinary function that returns once, so CGP realizes only the exactly-once fragment above. A [component](../reference/macros/cgp_component.md) is the effect signature, a [provider](../reference/macros/cgp_impl.md) is the handler, and [wiring](../reference/macros/delegate_components.md) installs the handlers on a context. The correspondence is exact for the tail-resumptive case and breaks cleanly wherever an effect would reach for the continuation. + +### Components are effect signatures; providers are (tail-resumptive) handlers + +A CGP component declares operations the way an effect declares them, and a provider interprets them the way a handler does — with the standing restriction that the interpretation is a plain function body. Declaring a component names a capability without giving it meaning: + +```rust +#[cgp_component(Greeter)] +pub trait CanGreet { + fn greet(&self); +} +``` + +`CanGreet` is the effect signature and `greet` the operation; a provider written with [`#[cgp_impl]`](../reference/macros/cgp_impl.md) is the handler, interpreting the operation for any context. The structural match is one-to-one — signature, operation, handler, installation — but the provider's body is not handed a continuation and cannot choose whether to resume. It computes a value and returns, and the caller resumes in place, exactly once. In effect-handler terms every CGP provider is a `fun` operation in Koka's sense, never a `ctl` one; there is no `resume` to call zero times or twice, because there is no reified continuation at all. + +### Reading from the context is dynamic binding, exactly + +The place the correspondence is not merely structural but *exact* is context-value access, because that is dynamic binding on both sides. Koka's `with fun ask() 21` installs a tail-resumptive handler that supplies a value to deeply nested code without threading it through every call; CGP's [getters](../concepts/implicit-arguments.md) and [`#[implicit]` arguments](../reference/attributes/implicit.md) do the same by reading a field from the context that is threaded through every provider as `self`: + +```rust +#[cgp_fn] +pub fn greet(&self, #[implicit] name: &str) -> String { + format!("Hello, {name}!") +} +``` + +The `name` argument is supplied not by the caller but from the context, precisely as Koka's `ask()` is supplied by the enclosing `ask` handler rather than by `add-twice`'s caller. This is the same dynamic-binding pattern the [implicit-parameters](implicit-parameters.md) document describes, and the equivalence is not a loose analogy: the reader effect *is* the canonical tail-resumptive handler, and reading a context field *is* CGP's whole realization of it. Where the two part ways is *how the value is found* — Koka searches the dynamic handler stack, CGP reads a field of a statically-known context — which is the same resolution split that separates CGP from every dynamically-scoped mechanism. + +### Raising an error looks like the `raise` operation — but passes a value, not control + +CGP's error handling is the sharpest illustration of the boundary, because it mimics the *selection* of an exception handler while doing none of the *control transfer*. [`CanRaiseError`](../reference/components/can_raise_error.md) reads like the `raise` operation, and a provider raises without knowing the concrete error type: + +```rust +#[cgp_impl(new LoadOrFail)] +#[uses(CanRaiseError)] +#[use_type(HasErrorType.Error)] +impl Loader { + fn load(&self, path: &str) -> Result { + if path.is_empty() { + return Err(Self::raise_error("empty path".to_owned())); + } + Ok(format!("contents of {path}")) + } +} +``` + +Because the raise-and-wrap components carry per-source-type dispatch, a context can even route each source error to a different strategy, which reads like installing one exception handler per exception type — `RaiseFrom` for a `String`, `DebugError` for a `ParseError` — through the [`open` statement](../reference/macros/delegate_components.md) exactly as [modular error handling](../concepts/modular-error-handling.md) describes. But the resemblance stops at *selection*. An effect `raise` is the *zero-resume* handler: it abandons the continuation and unwinds to the handler. CGP's `raise_error` does not unwind anything — it *constructs and returns a value* of the abstract `Self::Error` type, and the actual abort is Rust's own `return`/`?` on the `Result`, entirely outside the wiring. CGP selects the *interpretation* of the error (the handler-choice half) but leaves the *control flow* (the continuation-discarding half) to `Result`. This is why CGP's dispatch is not even one-shot like OCaml's — it is strictly exactly-once, because the zero-resume case never lives inside the capability system at all. + +### Impl-side dependencies are the effect row; `check_components!` is "all effects handled" + +CGP tracks which capabilities a provider needs, and verifies they are all supplied, in a way that lines up closely with an effect system typing a row and demanding it be discharged. A provider's needs are stated as [impl-side dependencies](../concepts/impl-side-dependencies.md) — the `#[uses(CanRaiseError)]` above, and every bound in a `where` clause — which is the CGP counterpart of the effect row `` that Koka would infer for a function that performs `raise`. A generic provider over `Context` with such bounds is even effect-*polymorphic* in a loose sense: the context type variable plays a role like Koka's row variable, standing for "whatever else this context can do." And [`check_components!`](../reference/macros/check_components.md) is the discharge check: + +```rust +check_components! { + App { + LoaderComponent, + } +} +``` + +This asserts that `App` supplies every capability `LoadOrFail` transitively needs, and fails to compile naming the missing one if not — which is exactly Koka's guarantee that a program with an unhandled effect in its row is a type error, and the opposite of OCaml's runtime `Effect.Unhandled`. CGP lands on the statically-checked end of the effect-typing axis, reached through trait resolution rather than a dedicated effect system. + +### Configuring abstract types through the same wiring + +CGP extends the operations-and-handlers wiring to something effect systems do not touch: abstract *types*. Alongside choosing the provider for an operation, a context chooses the concrete type behind an [abstract type](../concepts/abstract-types.md) — its `Error`, its `Scalar`, its `Runtime` — through the same [`delegate_components!`](../reference/macros/delegate_components.md) table, by wiring a [`#[cgp_type]`](../reference/macros/cgp_type.md) component to [`UseType`](../reference/providers/use_type.md): + +```rust +delegate_components! { + App { + ErrorTypeProviderComponent: UseType, + } +} +``` + +An effect handler interprets operations, which are values and computations; it has no notion of supplying a *type member* the way `HasErrorType` supplies `Self::Error`. CGP unifies both under one wiring mechanism, so the same table that says "raise errors this way" also says "and the error type is `anyhow::Error`." This is a genuine capability beyond the effect-handler analogy, not a restatement of it. + +### What CGP cannot express + +The multi-shot power of effect handlers has no CGP analogue, and this is the honest limit of the comparison. Because a provider is an ordinary function that returns once, CGP cannot express any effect whose handler resumes the continuation zero times or more than once. Generators, backtracking search, cooperative scheduling, and async/await — the applications that motivate effect handlers in the first place ([Kammar, Lindley & Oury, *Handlers in Action*](https://denotational.co.uk/publications/kammar-lindley-oury-handlers-in-action.pdf)) — all require capturing the continuation and are simply outside CGP's model. CGP does have an [async handler family](../concepts/handlers.md) and [type-level DSLs](../concepts/type-level-dsls.md) that interpret a `Code` tag by dispatching to a provider, which is the closest CGP comes to the operations-and-interpreters shape; but even there the interpretation is a straight call chain resolved at compile time, not a captured continuation the handler controls. Async in CGP is Rust's own `async`/`await` threaded through provider calls, not a handler that suspends and resumes a computation. + +## What users like and dislike + +Algebraic effects are among the most admired ideas in current language research, and the reasons practitioners value them are consistent and concrete. The headline benefit is the elimination of *function coloring*: intermediate code between the operation and its handler needs no awareness of the effect, so effectful and pure code compose without the `async`/`sync` or `IO`-tainted split that plagues other approaches ([Abramov, *Algebraic Effects for the Rest of Us*](https://overreacted.io/algebraic-effects-for-the-rest-of-us/)). Users also prize the clean separation between an effect's *interface* — its operations — and its *semantics* — the handler — which makes the same code testable under a mock handler and runnable under a real one, and the *composability* of stacking several handlers to combine independent effects, widely contrasted favorably with monad transformers and their pain ([*Why Algebraic Effects?*, Ante](https://antelang.org/blog/why_effects/)). Where the effects are typed, as in Koka and Flix, the row or set in a signature is valued as honest documentation of what a function may do, and Flix turns that information into automatic parallelization and dead-code elimination. + +The complaints are equally consistent and fall into three clusters. The loudest is *unfamiliarity and control-flow opacity*: the concept is new to most programmers, no mainstream language ships it, and — the recurring technical objection — effects "abstract away side effects to effect handlers by definition," so that following control from a `perform` to the handler that catches it is as hard as reasoning about a distant exception handler, the "which handler runs this?" problem ([Ante](https://antelang.org/blog/why_effects/)). The second is *performance*: general handlers must capture continuations, which costs, and while optimizing compilers recover much of it for the tail-resumptive case through evidence passing, non-tail handlers remain more expensive than native control flow ([Xie & Leijen 2021](https://xnning.github.io/papers/multip.pdf)). The third is language-specific and cuts against the untyped designs: OCaml's decision to omit effect typing means a function's signature says nothing about the effects it performs and an unhandled effect is a runtime crash rather than a compile error ([OCaml manual](https://ocaml.org/manual/5.5/effects.html)), and its one-shot restriction rules out the multi-shot uses outright. For readers who reach effects from Haskell, the nearest comparison is the type-class-based effect libraries (`mtl` and its successors), whose recurring frustration is the *n² instances* problem — every handler must supply instances delegating all the *other* effects — which handler-based systems avoid ([`fused-effects`](https://hackage.haskell.org/package/fused-effects); [`effet`](https://hackage.haskell.org/package/effet)). + +## How CGP compares + +CGP and algebraic effects make opposite choices on the two axes that define the effect-handler design space — *what a handler may do* and *how it is chosen* — and the comparison is cleanest stated as that pair of trades. On *handler power*, an effect handler holds the continuation and may resume it any number of times, which buys generators, backtracking, and async; a CGP provider is a plain function that returns once, which buys nothing control-theoretic but costs nothing either — the call monomorphizes to a direct jump with no continuation to capture. On *handler selection*, an effect handler is chosen by *dynamic scope* — the nearest handler on the runtime stack wins, and a program can install a fresh handler for the same effect at any point — while a CGP provider is chosen by the context's *type* at compile time, fixed once in a wiring table and resolved through the trait system. That second axis places CGP much closer to type classes and implicit parameters than to effects, which is why the [dependency-injection](dependency-injection.md) and [implicit-parameters](implicit-parameters.md) comparisons cover ground this one deliberately leaves to them. The bridge between the two paradigms is evidence passing: Koka *compiles* dynamically-scoped handlers down to dictionary passing for the tail-resumptive case, and CGP is what you get if that compiled form is the only form there ever was — evidence passing chosen by types, with no dynamic search beneath it. + +Two further divergences follow from these and are worth stating plainly. First, an effect handler *stack* is ordered and nesting matters: the innermost handler for an operation wins, and reordering handlers changes results — state-over-nondeterminism and nondeterminism-over-state compute different things ([Kammar, Lindley & Oury 2013](https://denotational.co.uk/publications/kammar-lindley-oury-handlers-in-action.pdf)). CGP's wiring is a *flat* table: one provider per component, resolved by type, with no dynamic nesting to shadow an outer handler and no order to permute — and because the dispatch is exactly-once and resume-in-place, the non-commutativity that makes handler order significant simply does not arise. A CGP table is a set of handlers, not a stack of them. Second, CGP keeps even less of the "algebraic" than the practical effect languages do: it has no equational theory relating its operations, though since Koka, OCaml, and Flix mostly drop the laws too, this is a shared simplification rather than a CGP-specific gap. + +Neither design dominates, and the honest positioning names where each wins. When a program needs the continuation — a scheduler, a generator, a backtracking solver, a suspendable coroutine — algebraic effects are the right and only tool of the two, and emulating them with CGP is not possible, not merely awkward. When a program needs many interchangeable implementations of a capability chosen per deployment, checked statically, compiled to zero-overhead direct calls, and extended to abstract types as well as operations — and needs none of the continuation power — CGP delivers that on stable Rust, where an effect system would require a language the platform does not have. The exactly-once fragment CGP restricts itself to is not a crippled effect system; it is dynamic binding and dictionary passing, which is a complete and useful thing in its own right, and CGP adds to it the static per-context selection and abstract-type configuration that effect handlers do not offer. + +## Presenting CGP to someone who knows this + +A reader fluent in algebraic effects arrives with most of CGP's structure already in mind, and the fastest way in is to map the vocabulary and then immediately mark the one boundary. A **component is an effect signature**, its methods are **operations**, a **provider is a handler**, and **wiring is installing the handlers** on a context; a provider's `where` bounds are the **effect row** it performs, and **`check_components!` is the guarantee that every effect is handled**, the static version of Koka's unhandled-effect type error rather than OCaml's runtime crash. Reading a context field is **dynamic binding** — and here the correspondence is exact, not approximate, because dynamic binding *is* the canonical tail-resumptive handler and reading a field is CGP's whole realization of it. Framed this way, CGP is not a foreign paradigm to this reader but the *tail-resumptive corner* of the one they know, made static and type-directed. + +The boundary to draw at once, before it misleads, is the continuation. This reader will assume a CGP provider can resume, abort, or fork the computation the way a handler can, and it cannot: a provider is an ordinary function that returns exactly once, so there is no `resume` to call zero times or twice, and every multi-shot use they value — generators, async, backtracking — is outside CGP entirely. Present that not as a missing feature but as the deliberate location of CGP in the design space: it takes the fragment of effect handlers that the literature already identifies as dynamic binding, the fragment that compiles to direct calls with no continuation capture, and builds everything on it. The pitch that lands for this audience is "effect handlers minus the continuation, resolved by type instead of by dynamic scope, and extended to abstract types" — which reframes what could read as a limitation as a precise and defensible design choice. For the Koka or Flix reader specifically, lean on the shared row intuition: their effect row and CGP's impl-side dependencies are the same idea, and CGP's `check_components!` discharges it the same way their type checker does. For the OCaml reader, the resonant point is that CGP recovers the *static* discharge check their language chose to forgo, and does so without needing effect typing bolted onto the language. And for anyone who has fought monad transformers or `mtl`'s n² instances, the framing is that CGP composes capabilities without either — a flat table of per-context handlers, no stacking order to get wrong. + +The analogy to avoid is calling CGP "an effect system." It is not — it has no continuations, no dynamic scope, and no effect kind in the type system — and a reader sold on that framing will look for `resume` and feel misled when it is not there. Say precisely what CGP is: the exactly-once, resume-in-place fragment of effect handlers, which is dynamic binding, made into a compile-time, per-context, type-directed wiring mechanism that also configures abstract types. A reader who knows how much of effect-handler machinery exists to tame the continuation will recognize that a paradigm which never captures one has bought real simplicity, and will hear the trade as a considered one rather than a shortfall. + +## Sources + +The account of the related work draws on the primary research literature on algebraic effects and their compilation, the official documentation of Koka, OCaml, and Flix, and cited community writing for sentiment; the CGP snippets are drawn from the knowledge base's [consumer/provider](../concepts/consumer-and-provider-traits.md), [implicit-arguments](../concepts/implicit-arguments.md), and [modular error handling](../concepts/modular-error-handling.md) documents and verified against current macro behavior. + +- [Plotkin & Pretnar, *Handling Algebraic Effects* (LMCS 2013)](https://homepages.inf.ed.ac.uk/gdp/publications/handling-algebraic-effects.pdf) ([journal page](https://lmcs.episciences.org/705)) and [*Handlers of Algebraic Effects* (ESOP 2009)](https://homepages.inf.ed.ac.uk/gdp/publications/Effect_Handlers.pdf) — the foundational account of effects as operations with an equational theory and handlers as models interpreting them via the continuation. +- [Pretnar, *An Introduction to Algebraic Effects and Handlers*](https://www.eff-lang.org/handlers-tutorial.pdf) and [Bauer & Pretnar, *Programming with Algebraic Effects and Handlers*](https://www.researchgate.net/publication/221671686_Programming_with_Algebraic_Effects_and_Handlers) — an accessible tutorial and the Eff language, an untyped research realization. +- [Leijen, *Algebraic Effects for Functional Programming* (Koka)](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/08/algeff-tr-2016-v2.pdf) and [The Koka Programming Language book](https://koka-lang.github.io/koka/doc/book.html) — Koka's row-typed effects and the `ctl`/`fun`/`val` operation distinction. +- [Xie et al., *Effect Handlers, Evidently* (ICFP 2020)](https://www.microsoft.com/en-us/research/wp-content/uploads/2020/07/evidently-5f0b7dbc1a998.pdf) ([ACM](https://dl.acm.org/doi/abs/10.1145/3408981)) and [Xie & Leijen, *Generalized Evidence Passing for Effect Handlers* (ICFP 2021)](https://xnning.github.io/papers/multip.pdf) — the tail-resumptive-handler-is-dynamic-binding result and the evidence-passing (dictionary-passing) compilation of the exactly-once fragment. +- [OCaml manual, *Effect handlers*](https://ocaml.org/manual/5.5/effects.html) and [Sivaramakrishnan et al., *Retrofitting Effect Handlers onto OCaml* (PLDI 2021)](https://kcsrk.info/slides/handlers_edinburgh.pdf) — OCaml 5's `perform`/`continue` syntax, its one-shot continuations, its lack of effect typing, and the runtime `Effect.Unhandled`. +- [Flix Effect System documentation](https://doc.flix.dev/effect-system.html) and [Effects and Handlers](https://doc.flix.dev/effects-and-handlers.html), with [Madsen et al., *Programming with Purity Reflection* (ECOOP 2023)](https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.ECOOP.2023.18) — Flix's set-based effect system, its `eff`/`run … with handler` syntax, and purity reflection. +- [Kammar, Lindley & Oury, *Handlers in Action* (ICFP 2013)](https://denotational.co.uk/publications/kammar-lindley-oury-handlers-in-action.pdf) — handler composition, the significance of handler order, and the range of effects handlers express. +- [Abramov, *Algebraic Effects for the Rest of Us*](https://overreacted.io/algebraic-effects-for-the-rest-of-us/) and [*Why Algebraic Effects?* (Ante)](https://antelang.org/blog/why_effects/) — community accounts of what users value (no function coloring, composability, interface/semantics separation) and dislike (novelty, control-flow opacity, performance). +- [`fused-effects`](https://hackage.haskell.org/package/fused-effects) and [`effet`](https://hackage.haskell.org/package/effet) — the type-class-based effect libraries and the `mtl` n²-instances problem, for the Haskell reader's point of comparison. diff --git a/docs/related-work/dependency-injection.md b/docs/related-work/dependency-injection.md new file mode 100644 index 00000000..7da53d52 --- /dev/null +++ b/docs/related-work/dependency-injection.md @@ -0,0 +1,194 @@ +# Dependency injection + +Dependency injection (DI) is the practice of giving an object its collaborators from the outside instead of letting it construct them, and the frameworks built around it — Spring, Guice, Dagger, and their kin — automate the wiring so a large application's object graph assembles itself. CGP solves the same decoupling problem, but at compile time and without a container, so it is the concept a reader with an enterprise background is most likely to reach for when they first meet CGP wiring. + +## Purpose + +Every non-trivial program has to decide where its components get the things they depend on, and dependency injection is the answer that keeps those decisions out of the components themselves. A class that needs a database, an HTTP client, and a logger can create them in its constructor — hard-coding the concrete types — or it can accept them as parameters and let whoever builds it supply them. The second choice is dependency injection, and its payoff is decoupling: the class names only the *interfaces* it needs, so a test can pass fakes, a different deployment can pass different implementations, and the class never changes. The cost is that something has to do the supplying, and in a large graph that "something" is elaborate enough that frameworks exist to run it. + +This is exactly the problem CGP's wiring addresses, which is why the comparison matters. A DI framework assembles an object graph by matching each dependency to a provider; CGP assembles a context by matching each component to a provider. The vocabulary rhymes, the goal is identical — decouple what a piece of code *needs* from what supplies it — and the differences are all in the mechanism: a DI framework resolves the graph at runtime from reflection and configuration, while CGP resolves it at compile time through the trait system. A reader who understands why DI decouples code already understands why CGP does; the work is in showing them what changes when the resolution moves from runtime to types. + +## The concept in depth + +Dependency injection is a specific form of *inversion of control*: rather than a component reaching out to fetch its dependencies, control is inverted so the dependencies are handed to it. The idea predates any framework and is expressible in plain code — pass collaborators as constructor arguments — but the frameworks are what most practitioners mean by "DI," because they automate the assembly. The sections below cover the container-and-annotation model that Spring popularized, the module-and-binding model of Guice and Dagger, and the plain-code form that Rust already encourages. + +### The IoC container and beans (Spring) + +Spring's core is an *inversion-of-control container*: an object, the `ApplicationContext`, that instantiates, configures, and connects the application's objects — its *beans* — and manages their lifecycles. A class is marked as a bean with an annotation such as `@Component` or `@Service`, and the container discovers it by scanning the classpath. The container owns every bean it creates, so the application asks the container for a fully-assembled object rather than constructing one itself. + +```java +@Service +public class UserService { + // business logic lives here +} +``` + +The container is configured either by annotation-driven scanning, as above, or explicitly with a `@Configuration` class whose `@Bean` methods return the objects to manage. The explicit form is where a bean's construction is spelled out, including which implementation stands in for an interface: + +```java +@Configuration +public class AppConfig { + @Bean + public StorageClient storageClient() { + return new S3StorageClient(/* ... */); + } +} +``` + +### Constructor, setter, and field injection + +Once the container holds a set of beans, it injects each bean's dependencies by one of three mechanisms, and the choice among them is the most-discussed decision in day-to-day Spring. **Constructor injection** passes dependencies as constructor arguments, which makes them required and lets the field be `final`: + +```java +@Service +public class ProfilePictureService { + private final StorageClient storage; + private final UserRepository users; + + public ProfilePictureService(StorageClient storage, UserRepository users) { + this.storage = storage; + this.users = users; + } +} +``` + +**Setter injection** supplies a dependency through a setter after construction, which suits genuinely optional collaborators, and **field injection** writes the dependency straight into a private field by reflection, marked with `@Autowired`: + +```java +@Service +public class ProfilePictureService { + @Autowired private StorageClient storage; // field injection + @Autowired private UserRepository users; +} +``` + +The `@Autowired` annotation is the instruction to resolve a dependency by type: the container finds the one bean assignable to `StorageClient` and injects it. When a single constructor is present, Spring treats it as autowired without the annotation, which is why modern Spring code favors constructor injection and reserves `@Autowired` for the ambiguous or field-injected cases. The Spring team and the wider community now recommend constructor injection for all required dependencies, because it makes a class's dependencies explicit in its signature, keeps them non-null, and lets the object be built without a container in a test. + +### Modules and bindings (Guice and Dagger) + +Guice and Dagger express the same wiring not by classpath scanning but by explicit *bindings* declared in a *module*. A Guice module maps an interface to an implementation, and the injector resolves a request for the interface to the bound implementation: + +```java +public class StorageModule extends AbstractModule { + @Override + protected void configure() { + bind(StorageClient.class).to(S3StorageClient.class); + } +} +``` + +A class then requests its dependencies by annotating its constructor with `@Inject`, and the injector supplies them from the bindings. Dagger uses the same `@Inject`/module vocabulary but resolves the graph at *compile time*: its annotation processor generates the wiring code during the build, so a missing or ambiguous binding is a compile error and there is no reflection at runtime. This compile-time-versus-runtime split is the sharpest axis of variation among DI frameworks — Spring and Guice resolve bindings at runtime through reflection, Dagger resolves them at compile time through generated code — and it is the axis along which CGP sits firmly at the compile-time end. + +### Dependency injection without a framework (Rust) + +Rust practitioners generally hold that the language needs no DI framework, because traits and generics already provide the decoupling a container is built to deliver. A function that needs a capability takes a generic parameter bounded by a trait; a caller supplies any type implementing that trait; a test supplies a fake. Construction is separated from use by the ordinary discipline of taking collaborators as arguments rather than building them internally. + +```rust +trait StorageClient { + fn fetch(&self, object_id: &str) -> Vec; +} + +struct ProfilePictureService { + storage: S, +} +``` + +This is dependency injection in the original sense — dependencies supplied from outside, interfaces instead of concrete types — with the compiler doing the checking and no runtime container involved. Its limitation is the one CGP exists to lift: a generic bound like `S: StorageClient` leaks into every caller's signature, and coherence permits only one `impl StorageClient` per type, so swapping implementations per application, or offering several interchangeable ones, runs into the same walls that motivate CGP's [consumer/provider split](../concepts/coherence.md). CGP is best introduced to a Rust audience as the next step along this line they already accept, not as an import of the container model they have rejected. + +## How CGP expresses it + +CGP performs dependency injection through two mechanisms working together: a provider declares what it needs as [impl-side dependencies](../concepts/impl-side-dependencies.md), and a context supplies them by [wiring](../concepts/consumer-and-provider-traits.md) each component to a provider. The impl-side dependency is the counterpart of a constructor's parameter list, and the wiring table is the counterpart of the container's configuration — but both are resolved by the compiler, so the "container" has no runtime existence at all. + +### Impl-side dependencies are the injected constructor parameters + +A provider states the collaborators and values it needs in a way that reads like declaring dependencies, and CGP satisfies them from the context rather than from a container. Where a Spring service lists `StorageClient` and `UserRepository` as constructor parameters, a CGP provider lists its capability dependencies with [`#[uses(...)]`](../reference/attributes/uses.md) and its value dependencies with [`#[implicit]`](../reference/attributes/implicit.md) arguments. A user-creation provider that needs a database connection and a censorship service declares both: + +```rust +#[cgp_impl(new PostgresUserManager)] +#[uses(CanCensorUsername)] +#[use_type(HasErrorType.Error)] +impl UserManager { + fn create_user( + &self, + #[implicit] database: &PostgresDb, + username: &str, + email: &Email, + ) -> Result { + if self.username_is_censored(username) > Probability::new(0.8) { + return Err(Error::InvalidUsername); + } + // ... insert the user with `database` + } +} +``` + +The `#[uses(CanCensorUsername)]` line injects a *capability* — the same role a `UserRepository` collaborator plays in the Spring constructor — and the `#[implicit] database` argument injects a *value* pulled from the context's `database` field, the role a configuration bean plays. Neither dependency appears in the `CanManageUser` consumer trait a caller invokes, so, unlike a leaked generic bound, they do not cascade to callers — the decoupling a DI framework promises, delivered by hiding the requirements one level down in the provider's impl rather than in a container. + +### Wiring is the container configuration + +A context selects which provider satisfies each component in a [`delegate_components!`](../reference/macros/delegate_components.md) table, which is the direct analogue of a Spring `@Configuration` class or a Guice module: the one place where interfaces are mapped to implementations. Swapping an implementation is a one-line edit to this table, and — the crucial difference — two contexts can map the same component to different providers with no conflict, because the choice is keyed on the context type. A profile-picture service backed by different object stores in different deployments is two wiring tables: + +```rust +#[cgp_component(StorageObjectFetcher)] +pub trait CanFetchStorageObject { + fn fetch_storage_object(&self, object_id: &str) -> anyhow::Result>; +} + +delegate_components! { + App { + StorageObjectFetcherComponent: FetchS3Object, + } +} + +delegate_components! { + GCloudApp { + StorageObjectFetcherComponent: FetchGCloudObject, + } +} +``` + +`FetchS3Object` and `FetchGCloudObject` are interchangeable providers of the same capability — the CGP equivalent of two beans bound to one interface — and the wiring picks one per context. Because the selection is resolved during type-checking and monomorphized to a direct call, the `App` binary contains only the S3 code path and the `GCloudApp` binary only the GCloud one, with no runtime dispatch. A DI container makes the same substitution, but by holding both implementations and choosing at startup from configuration. + +### Checking replaces the container's startup validation + +A DI container discovers a missing or ambiguous binding when it assembles the graph — at startup for Spring and Guice, at build time for Dagger. CGP's counterpart is [`check_components!`](../reference/macros/check_components.md), which asserts at compile time that a context's wiring is complete and every provider's transitive dependencies are satisfied: + +```rust +check_components! { + App { + StorageObjectFetcherComponent, + } +} +``` + +If `FetchS3Object` needs a field or capability the `App` context does not supply, this fails to compile with the missing dependency named, rather than surfacing as a startup exception or a `NullPointerException` deep in a request. It is the same guarantee Dagger gives — the graph is verified before the program runs — reached through the trait system instead of an annotation processor. CGP wiring is [lazy](../concepts/check-traits.md), so this check is what turns a latent gap into an early, readable error. + +## What users like and dislike + +Dependency-injection frameworks are among the most widely adopted tools in enterprise software, and the reasons practitioners value them are real. They decouple components from their collaborators, which makes code testable — a fake is injected exactly where a real dependency would be — and swappable across environments. They centralize wiring, so the shape of an application's object graph lives in one readable place rather than scattered through constructors. And a mature framework like Spring brings an enormous ecosystem: transaction management, security, web bindings, and configuration all keyed off the same bean model, so adopting the container brings far more than injection. + +The complaints are equally well-documented, and they cluster around the runtime, reflective nature of the popular frameworks. The most common is that dependencies become *hidden*: with field injection, a class's signature says nothing about what it needs, so a reader must scan the whole class and a maintainer can add a dependency invisibly. Reflection-based resolution means a missing or mis-typed binding is a *runtime* failure — a startup exception or a `NullPointerException` in production — not a compile error, which is why the Spring community now steers hard toward constructor injection and away from field injection. There is a performance and startup cost to scanning the classpath and building the graph reflectively, which is why Dagger's compile-time generation exists and why it wins on Android and in latency-sensitive services. And there is the recurring complaint of *magic*: the framework does so much automatically, through reflection and proxies, that when something goes wrong the developer has little visibility into why, and the behavior is hard to reason about from the code alone. Even proponents concede the learning curve is steep and the machinery is heavy for a small program. + +## How CGP compares + +CGP takes the compile-time end of every axis the DI frameworks vary along, which is its central trade-off: it gives up runtime flexibility to gain static guarantees and zero cost. Because wiring is resolved by the trait system and monomorphized, there is no container, no reflection, no runtime graph, and no dynamic dispatch — a wired call compiles to a direct function call, and an unused provider is not in the binary. A dependency that a context fails to satisfy is a compile error at the wiring site, not a startup exception, so the class of failures that field injection is criticized for cannot occur. And a provider's dependencies are never hidden: they are stated in its `#[uses]` and `#[implicit]` declarations and enforced by the compiler, giving the explicitness the Spring community prizes in constructor injection, by default. + +The costs are just as real and should be stated plainly. CGP resolves everything at compile time, so it cannot do what a runtime container does best: reconfigure an application without recompiling, load plugins chosen at startup from a config file, or build a graph whose shape is not known until the program runs. It is confined to Rust, whereas Spring anchors a vast cross-cutting ecosystem that injection is only the entry point to. Its machinery — the consumer/provider split, [coherence-bypassing](../concepts/coherence.md) wiring, type-level tables — has a learning curve of its own, and its error messages, though they name the missing dependency under a check, can be verbose. A DI framework remains the better choice when the application genuinely needs runtime reconfiguration, when it lives in a JVM or .NET ecosystem whose libraries assume the container, or when the team's familiarity with the framework outweighs the benefits of static wiring. CGP wins where the graph is known at build time and the guarantees and zero cost matter: systems programming, latency-sensitive services, and libraries that must not impose a runtime. + +## Presenting CGP to someone who knows this + +A reader who knows dependency injection arrives with the right instinct — decouple what code needs from what supplies it — and the fastest way in is to map their vocabulary onto CGP's directly. A **provider** is a bean or a binding: an interchangeable implementation of a capability. **Wiring** with `delegate_components!` is the container configuration — the `@Configuration` class or the Guice module — the single place where interfaces are matched to implementations. An **impl-side dependency** is a constructor parameter: what a provider needs from the outside, declared where the implementation lives and never leaked to callers. And **`check_components!`** is the graph validation a container runs — the difference being *when* it runs. Leading with this dictionary lets the reader reuse everything they know about why DI decouples code, and spend their attention only on what is new. + +The one analogy to defuse immediately is the runtime container. A DI-trained reader will assume there is an object somewhere holding the graph, resolving dependencies by reflection, choosing implementations at startup — and there is not. CGP's "container" is the type system, the "graph" is a set of trait impls, and the resolution happens during compilation and compiles away to direct calls. Say this explicitly, because leaving it unsaid invites the reader to imagine a runtime cost and a runtime failure mode that do not exist. The framing that lands is *Dagger, taken further*: a reader who knows Dagger already accepts compile-time-verified injection with no reflection, and CGP is that same bargain with per-context choice added — the same interface can resolve to different implementations in different contexts, which a single global binding graph cannot express. + +The advantages worth foregrounding are the ones that answer this audience's own complaints. Dependencies are explicit and compiler-checked, so the hidden-dependency and `NullPointerException` failure modes that drove the community off field injection are gone by construction. There is no startup cost or reflection, so the performance objection that motivates Dagger is answered more completely. And nothing unused reaches the binary, which reads to this audience as automatic tree-shaking of the object graph. The expectation to address head-on is that CGP asks for explicit wiring and cannot scan-and-discover: there is no classpath scanning, no `@Component` auto-registration, no runtime rebinding, and the graph must be known at compile time. Present that not as a limitation grafted on but as the deliberate price of the guarantees — the same trade Dagger made, carried to its conclusion — and the reader who values static safety will read it as a feature rather than a loss. + +## Sources + +The account of the related work draws on the official framework documentation and representative community writing, cited where a specific claim rests on one. + +- [Spring Framework reference — Dependency Injection](https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html) — the authoritative description of the IoC container, beans, and the constructor/setter injection mechanisms. +- [Baeldung — Inversion of Control and Dependency Injection in Spring](https://www.baeldung.com/inversion-control-and-dependency-injection-in-spring) — the distinction between IoC and DI and the `@Autowired` autowiring-by-type behavior. +- [Comparing Dependency Injection Frameworks — Spring, Guice, Dagger, and Micronaut](https://medium.com/@AlexanderObregon/comparing-dependency-injection-frameworks-spring-guice-and-dagger-a614dccd5859) and [Dagger vs Guice](https://www.hackingnote.com/en/versus/dagger-vs-guice/) — the runtime-versus-compile-time split and the reflection-versus-generated-code trade-off across frameworks. +- [Field injection is not recommended (Marc Nuri)](https://blog.marcnuri.com/field-injection-is-not-recommended) and [James Shore — The Problem With Dependency Injection Frameworks](https://www.jamesshore.com/v2/blog/2023/the-problem-with-dependency-injection-frameworks) — the hidden-dependency, runtime-failure, and "magic" criticisms, and the case for constructor injection. +- [Rust traits and dependency injection (jmmv.dev)](https://jmmv.dev/2022/04/rust-traits-and-dependency-injection.html) — the position that Rust performs dependency injection through traits and generics without a framework. diff --git a/docs/related-work/dynamic-dispatch.md b/docs/related-work/dynamic-dispatch.md new file mode 100644 index 00000000..c4210761 --- /dev/null +++ b/docs/related-work/dynamic-dispatch.md @@ -0,0 +1,164 @@ +# Dynamic dispatch, dynamic typing, and prototypal inheritance + +Dynamically-typed languages resolve almost everything at runtime: a method call is *dynamically dispatched* to an implementation chosen from the receiver, an object's behavior is looked up in a *vtable* or a method dictionary, code is written in *duck-typed* style that trusts a value to respond to the messages sent to it, and shared behavior is inherited by *delegation* along a prototype chain. CGP reproduces the openness all of this buys — many interchangeable implementations behind one interface, behavior assembled by delegation, defaults inherited and overridden, and provider code that reads as if it just sends messages to an object — but resolves every bit of it at compile time into direct, monomorphized calls, so its vtable is a type-level table erased before the program runs and its prototype chain is walked by the type checker rather than the CPU. + +## Purpose + +Dynamic dispatch exists to decouple a call site from the implementation it invokes, so that one piece of code works over many implementations chosen later. When code calls `shape.area()`, dynamic dispatch lets the concrete implementation be decided by the receiver rather than fixed where the call is written, which is what makes polymorphism, plugins, and open extension possible. Dynamically-typed languages take this to its limit: nothing about the receiver's type is fixed at compile time, so a value is whatever it can *do*, behavior is shared by pointing one object at another, and the whole program is malleable at runtime. The appeal is flexibility and immediacy — write against an interface without declaring it, extend without recompiling, explore in a REPL. + +This is the same decoupling CGP performs, which is why the comparison is illuminating rather than incidental, and the knowledge base already draws the connection: the [`delegate_components!`](../reference/macros/delegate_components.md) reference calls a context's wiring "a type-level table, analogous to an object's method table (vtable)," and the [bypassing coherence](../concepts/coherence.md) concept is careful to add that, unlike a real vtable, CGP's resolution is static and compiles down to direct calls with no runtime table and no dynamic dispatch. Both paradigms answer "how does a call reach an implementation chosen elsewhere?" — dynamic languages by looking it up at runtime, CGP by resolving it at compile time through the trait system. This document meets the reader who thinks in objects, messages, vtables, and prototypes, and shows where CGP's static mirror of those mechanisms lands. It is the object-oriented, dynamic-language counterpart to the type-theoretic [type classes](type-classes.md) and [ML modules](ml-modules.md) comparisons; the *type-system* side of duck typing — structural versus nominal typing — is developed in [row polymorphism](row-polymorphism.md), so this document leans on that one for the shape theory and keeps its own focus on dispatch, delegation, and the feel of the code. + +## The concept in depth + +Dynamic languages layer several ideas that a reader should keep distinct: *dynamic typing* and the *duck typing* style it enables, *dynamic dispatch* as the runtime resolution of a call, the *vtable* and method-dictionary machinery that implements it, and *prototypal inheritance* as the runtime sharing of behavior by delegation. The subsections build up in that order and close on the property of delegation — that `self` stays bound to the original object — that turns out to line up with CGP most precisely. + +### Dynamic typing and duck typing + +A dynamically-typed language checks types at runtime, and *duck typing* is the programming style this permits: an object's usability is decided by the methods and properties it actually has, not by a class it declares or an interface it implements. The maxim is "if it walks like a duck and quacks like a duck, then it is a duck" — a function that calls `x.quack()` works for *any* `x` that responds to `quack`, with no declared relationship between them ([*Duck typing*, Wikipedia](https://en.wikipedia.org/wiki/Duck_typing)). Code written this way sends messages to a value and trusts it to respond, deferring the question "does it actually have this method?" to the moment the call runs. Python, Ruby, JavaScript, and Smalltalk are the canonical homes of the style, and its whole appeal is that an interface need never be spelled out: you write the calls, and any object that can service them qualifies. + +### Dynamic dispatch and late binding + +Dynamic dispatch is the runtime selection of which implementation a method call invokes, based on the receiver. Also called *late binding*, it is the mechanism where the association between a call and the code it runs is resolved at runtime rather than at compile time, and it is what makes a single interface invoke different underlying methods depending on the object's actual type ([*Dynamic dispatch*, Wikipedia](https://en.wikipedia.org/wiki/Dynamic_dispatch)). Smalltalk gave the purest form: every call is a *message send*, resolved by a `send` operation that takes the receiver and the message name and, *at call time*, consults the receiver's class method dictionary to find the method to run ([*Dynamic dispatch*, Wikipedia](https://en.wikipedia.org/wiki/Dynamic_dispatch)). Statically-typed object languages — C++, Java, Rust — offer the same late binding for methods marked virtual, dispatching on the single receiver; a few languages (CLOS, Julia) generalize to *multiple dispatch*, choosing on the runtime types of several arguments at once. + +### Vtables and method dictionaries + +Dynamic dispatch is implemented, in compiled languages, by a *virtual method table*: a per-class array of function pointers, one slot per virtual method, that each object reaches through a hidden vtable pointer. A virtual call loads the vtable pointer from the object, indexes to the method's slot, and calls the function pointer found there — an indirection resolved entirely at runtime ([*Virtual method table*, Wikipedia](https://en.wikipedia.org/wiki/Virtual_method_table)). Rust's own dynamic dispatch works exactly this way: a `dyn Trait` value is a *fat pointer* pairing a data pointer with a vtable pointer, and the vtable is a statically-built table holding the type's destructor, size, and alignment followed by its method pointers, so a call through `dyn Trait` loads the vtable, looks up the method, and calls it with the data pointer ([geo-ant, *Rust Dyn Trait Objects and Fat Pointers*](https://geo-ant.github.io/blog/2023/rust-dyn-trait-objects-fat-pointers/); [*Trait objects*, The Rust Programming Language Book](https://doc.rust-lang.org/book/ch18-02-trait-objects.html)). The cost is real: an indirect call defeats inlining and adds a load per dispatch. Dynamically-typed languages pay even more, resolving a method by name through a dictionary up the class or prototype chain — which is why their implementations invest heavily in *inline caches* and *hidden classes*, techniques pioneered in the Self language's *maps* and *polymorphic inline caches* and inherited by V8 to make repeated same-shape access fast ([*Maps (Hidden Classes) in V8*](https://v8.dev/docs/hidden-classes)). The vtable is the enduring image: a table of function pointers, consulted at runtime, that maps an interface's methods to a type's implementations. + +### Prototypal inheritance and delegation + +Prototype-based languages share behavior not through classes but by *delegation*: an object holds a link to another object, its prototype, and a message the object does not handle is forwarded to the prototype, walking a chain until the message is answered or the chain ends. This model, introduced by Henry Lieberman's 1986 *Using Prototypical Objects to Implement Shared Behavior in Object-Oriented Systems* and realized in the Self language, needs no classes: an object is a blueprint for others, and behavior is inherited by pointing at it ([Lieberman, *Using Prototypical Objects…*](https://web.media.mit.edu/~lieber/Lieberary/OOP/Delegation/Delegation.html)). JavaScript is its most widely-used descendant — every object has a `[[Prototype]]` link, `Object.create(proto)` sets it, and a property lookup walks the prototype chain, with an own property *shadowing* an inherited one ([MDN, *Inheritance and the prototype chain*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain)). Lua expresses the same idea through the `__index` metamethod, and a missing method can even be caught by a fallback hook — Ruby's `method_missing`, Python's `__getattr__`, Lua's `__index` function. + +The property that distinguishes *delegation* from mere forwarding is the one that matters most for CGP: `self` stays bound to the original receiver. When an object delegates a message to its prototype and the prototype's method refers to `self`, that `self` denotes the object that *originally received* the message, not the prototype the method was found on ([Lieberman, *Using Prototypical Objects…*](https://web.media.mit.edu/~lieber/Lieberary/OOP/Delegation/Delegation.html)). This is what makes delegation a form of inheritance rather than plain message forwarding: the delegate supplies the *behavior*, but the original object remains the *identity* that behavior runs against, so a method inherited from a prototype still sees the receiver's own state. Forwarding, by contrast, rebinds `self` to the object the message was passed to, losing that connection. The delegation-keeps-self-bound rule is the precise hinge on which the CGP comparison turns. + +## How CGP expresses it + +CGP reproduces dynamic dispatch, vtables, and prototypal delegation, but resolves each at compile time, so what is a runtime lookup in a dynamic language is a type-resolution step in CGP that monomorphizes to a direct call. A [component](../reference/macros/cgp_component.md) is the interface, a [provider](../reference/macros/cgp_impl.md) is an implementation, the context's [wiring table](../reference/macros/delegate_components.md) is its vtable, and [aggregate providers](../concepts/aggregate-providers.md) and [namespaces](../concepts/namespaces.md) are its prototype chain. The correspondence is close construct by construct, and it breaks in exactly one place — everything is static — which is the source of both what CGP gains and what it gives up. + +### CGP code reads like a duck-typed program + +The most immediate resemblance is stylistic: a CGP provider is written against an unknown context and reads like duck-typed code that sends messages to a receiver and trusts it to respond. A provider calls methods on `self` and reads values from it without ever naming a concrete type or declaring the interface it depends on: + +```rust +#[cgp_impl(new GreetHello)] +#[uses(HasName)] +impl Greeter { + fn greet(&self) { + println!("Hello, {}!", self.name()); + } +} +``` + +The body `self.name()` is a message send to a context whose type is not written down, and with an [`#[implicit]`](../reference/attributes/implicit.md) argument the dynamic feel is stronger still — a value simply materializes from the context, as an unbound name would in Ruby or Python: + +```rust +#[cgp_fn] +pub fn greet(&self, #[implicit] name: &str) -> String { + format!("Hello, {name}!") +} +``` + +This is nothing like ordinary statically-typed Rust, where calling `x.name()` demands a concrete `x: Person` or a spelled-out bound `fn greet(c: &C)` that names the capability in the signature. CGP hides the bound behind [`#[uses]`](../reference/attributes/uses.md) and abstracts the context away, so the provider body carries *less* visible type ceremony than a plain generic function and reads as if it were duck-typed — "assume the context has a `name`, and greeting works." The decisive difference is *when* the trust is discharged. A duck-typed program finds out at runtime whether the object responds, failing with an `AttributeError` or `NoMethodError` if not; CGP finds out at compile time, because the `#[uses(HasName)]` dependency and the field access are checked by trait resolution and by [`check_components!`](../reference/macros/check_components.md) — which is Ruby's `respond_to?` guard moved to compile time and made total. And the field-based half of this is genuinely structural, not merely stylistic: an `#[implicit]` argument or a [`#[cgp_auto_getter]`](../reference/macros/cgp_auto_getter.md) resolves against *any* context carrying a matching field, the duck-typing-as-a-type-system idea that [row polymorphism](row-polymorphism.md) develops in full. CGP code looks duck-typed on the surface while its resolution stays static and, at the wiring, nominal. + +### Static dispatch with the flexibility of dynamic dispatch + +CGP delivers the openness of dynamic dispatch — one interface, many implementations, the choice made late — but resolves the choice at compile time and compiles it to a direct call. A caller writes `context.area()` against the `CanCalculateArea` consumer trait without naming an implementation, exactly as a dynamic call names a method and lets the receiver decide; the difference is that CGP's "receiver decides" happens during type checking. The consumer-trait blanket impl generated by [`#[cgp_component]`](../reference/macros/cgp_component.md) routes the call through the context's [`DelegateComponent`](../reference/traits/delegate_component.md) table to the wired provider, and the whole routing is resolved by the compiler and [monomorphized to a direct, zero-cost call](../concepts/coherence.md) — no fat pointer, no vtable load, no indirect jump. Rust already offers *real* dynamic dispatch through `dyn Trait`, and CGP is deliberately its static sibling: both let an implementation be chosen after the calling code is written, one paying a runtime vtable indirection to do it and the other resolving it away entirely. Where a component carries a generic parameter, CGP even reproduces *multiple* dispatch — the [`open` statement](../reference/macros/delegate_components.md) selects a provider per value of a type argument, dispatching on the context *and* that argument the way multimethods dispatch on several runtime types, but decided at compile time. + +### `DelegateComponent` is a compile-time vtable + +The type-level table a context carries is a vtable that exists only during compilation. Each [`DelegateComponent`](../reference/traits/delegate_component.md) impl on a context maps one component key to the provider that implements it, precisely as a vtable slot maps a method to its implementation: + +```rust +delegate_components! { + Rectangle { + AreaCalculatorComponent: RectangleArea, + } +} + +// expands to the type-level table entry: +// impl DelegateComponent for Rectangle { +// type Delegate = RectangleArea; +// } +``` + +The mapping to a vtable is exact on structure and opposite on timing. A context type plays the role of a class, a `DelegateComponent` impl is a vtable slot, and the provider it names is the function that slot points to — but the key is a *type* (the component marker) rather than a method offset, the lookup is performed *once by the compiler* rather than on every call by the CPU, and the table is *erased* before the program runs rather than materialized as data an object points to. This is why the [`delegate_components!`](../reference/macros/delegate_components.md) reference introduces the table as "analogous to a vtable… resolved at compile time" — the analogy is precise, and the qualification is the whole point. The honest cost of resolving the vtable away is that CGP loses the runtime heterogeneity a real vtable enables: a `Vec>` can hold different concrete shapes and dispatch each at runtime, whereas a CGP context is one monomorphic type resolved once, so mixing implementations in a single collection and choosing between them at runtime is the job of Rust's `dyn`, not of CGP. + +### Component delegation is delegation, with `self` bound + +CGP's delegation chain is delegation in Lieberman's exact sense, because the context stays bound to the original as lookup walks the chain. An [aggregate provider](../concepts/aggregate-providers.md) bundles a group of component wirings, and a context delegates a whole group to it in one entry, so resolution walks from the context through the bundle to a leaf provider: + +```rust +delegate_components! { + new GeometryComponents { + AreaCalculatorComponent: RectangleArea, + PerimeterCalculatorComponent: RectanglePerimeter, + } +} + +delegate_components! { + Rectangle { + [AreaCalculatorComponent, PerimeterCalculatorComponent]: GeometryComponents, + } +} +``` + +When `rect.area()` resolves, the lookup walks `Rectangle` → `GeometryComponents` → `RectangleArea`, and — this is the crux — the *context stays `Rectangle`* at every step: the [aggregate-providers concept](../concepts/aggregate-providers.md) states outright that "the context argument is `Rectangle` at every step; `GeometryComponents` appears only in the `Self`/delegate position, never as the `Context`," so the leaf provider reads its fields and capabilities from `Rectangle`, the real context, not from the bundle. That is delegation's defining property precisely: the delegate chain supplies the *behavior* while `self` — the context — remains the original *identity* that behavior runs against, never rebound to the prototype the method was found on. The [`UseContext`](../reference/providers/use_context.md) provider is the same relationship pointed the other way, letting a provider route a call back to the context's own wiring — the prototype consulting its delegator. CGP's delegation is not analogous to prototypal delegation; on the axis that separates delegation from forwarding, it *is* delegation, resolved statically. + +### Namespaces are prototypal inheritance with override + +A CGP namespace is a prototype: a reusable object of default wirings that a context inherits and then selectively shadows. A context joins a namespace and inherits every entry it does not wire directly, and a directly-wired entry on the context *wins* over the inherited one — own-property-shadows-prototype, expressed at the type level ([namespaces concept](../concepts/namespaces.md)): + +```rust +delegate_components! { + App { + namespace DefaultNamespace; // inherit the namespace's wirings as defaults + + // a directly-wired entry here shadows the namespace's entry for that key + } +} +``` + +Namespaces inherit from one another exactly as prototypes chain into further prototypes, so a base namespace can be extended into a richer one that contexts downstream pick up: + +```rust +cgp_namespace! { + new ExtendedNamespace: DefaultNamespace { + @cgp.core.error => @app, + } +} +``` + +`ExtendedNamespace` resolves everything `DefaultNamespace` does plus its own entries, the prototype-of-a-prototype relationship at the wiring level. The lookup that walks these layers is the [`RedirectLookup`](../reference/providers/redirect_lookup.md) provider tracing a type-level path, which is the prototype chain being walked — and the redirect that fires when a key is not directly present is the compile-time analogue of a `__index` or `method_missing` fallback. As with everything else in CGP, the chain is walked by trait resolution during compilation rather than by property lookup at runtime, so the inheritance is real but pays nothing at runtime and cannot be mutated once the program is built. + +## What users like and dislike + +Dynamic dispatch and dynamic typing are loved for the immediacy and flexibility they give, and the praise is consistent across their communities. Duck typing lets a function work over any object that responds to the messages it sends, which programmers value for *flexibility and reuse* — no interface to declare, no hierarchy to fit into — and for *cleaner, shorter code* and *rapid prototyping*, the reasons Ruby and Python developers reach for it ([SitePoint, *Making Ruby Quack*](https://www.sitepoint.com/making-ruby-quack-why-we-love-duck-typing/); [GeeksforGeeks, *Type Systems*](https://www.geeksforgeeks.org/python/type-systemsdynamic-typing-static-typing-duck-typing/)). Dynamic dispatch is what makes polymorphism and open extension work, and prototypal inheritance is prized for its malleability — objects can be created, linked, and modified at runtime, behavior can be shared without a class hierarchy, and a running program can be reshaped live. Metaprogramming hooks like `method_missing` and `__getattr__` let a single object answer messages it was never written to handle, which powers proxies, DSLs, and ORMs. + +The complaints are the mirror image of that flexibility, and they cluster on safety and cost. Because type checks are deferred, a mistake surfaces as a *runtime error* — an `AttributeError`, a `NoMethodError`, an "undefined is not a function" — discovered only when the offending line runs, which makes refactoring hazardous and large systems harder to trust; the standard mitigation is to add tests, type annotations, or a `respond_to?` guard before the call ([DevGex, *Duck Typing*](https://devgex.com/en/article/00035033); [SitePoint](https://www.sitepoint.com/making-ruby-quack-why-we-love-duck-typing/)). Dynamic dispatch has a *performance cost* — the vtable indirection defeats inlining, and dictionary-based lookup in dynamic languages is worse, which is why so much engineering goes into inline caches and hidden classes to claw it back ([V8, *Hidden Classes*](https://v8.dev/docs/hidden-classes)). Prototypal inheritance draws its own specific gripes: a mutable prototype chain is easy to get confused about, and JavaScript's `this` binding — the very self-binding that makes delegation work — is a notorious source of bugs when a method is detached from its receiver. And tooling suffers throughout, since an IDE cannot reliably know what a value responds to when its type is not fixed. + +## How CGP compares + +CGP takes the static end of every axis these mechanisms define, which is its central trade: it keeps the openness and gives up the runtime. On *dispatch*, CGP resolves at compile time and monomorphizes where a dynamic language resolves at runtime, so there is no vtable, no method-dictionary lookup, and no indirect call — a wired call is a direct one, and nothing about it survives into the running program. On *typing*, CGP writes duck-typed-looking provider code but checks it statically, so the missing-method failure that a dynamic language hits at runtime is a compile error at the wiring site, caught by `check_components!` rather than by a production stack trace. On *inheritance*, CGP's delegation and namespace chains are walked by the type checker, not by runtime property lookup, so they cost nothing and cannot go wrong through a mis-set prototype or a detached `this`. Each side pays for what the other gets. Dynamic languages get runtime malleability — heterogeneous collections, plugins loaded at startup, live redefinition, metaprogramming — and pay with runtime errors and dispatch overhead; CGP gets zero-cost dispatch and static guarantees and pays by fixing the wiring at compile time, so nothing about it can change while the program runs. + +The costs on CGP's side are worth naming plainly, because they are exactly the capabilities dynamic dispatch exists to provide. CGP cannot hold a heterogeneous collection of implementations and choose among them at runtime — that is what Rust's own `dyn Trait` is for, and a program that needs it should reach for a trait object, not for CGP. It cannot load an implementation chosen at runtime from configuration or a plugin file, monkey-patch a live object, or synthesize a response to a message it was not built to handle, because there is no runtime object graph to mutate and no runtime dispatch to intercept. And its errors, though caught early, are the verbose generated-type messages the [check traits](../concepts/check-traits.md) exist to localize, rather than the direct `NoMethodError` a dynamic language reports. + +Neither is uniformly better, and the honest positioning names where each wins. When a program needs runtime openness — plugins discovered at startup, objects of mixed types in one collection, behavior reshaped live, or the metaprogramming that proxies and DSLs rely on — dynamic dispatch is the right tool, and emulating it with CGP's compile-time wiring is not merely awkward but impossible, since the whole point is deferral to runtime. When a program's set of implementations is known at build time and it wants the decoupling of dynamic dispatch with none of the cost or the runtime failure modes — polymorphism that inlines, duck-typed ergonomics that cannot throw `NoMethodError`, and inheritance that the compiler resolves — CGP delivers that on stable Rust, and does so as ordinary types with no runtime machinery at all. + +## Presenting CGP to someone who knows this + +A reader who thinks in objects, messages, and prototypes holds most of CGP's structure already, and the way in is to map the vocabulary and then flag the one change. A **context is an object**, a **component is a message or method**, a **provider is a method implementation**, **wiring is the object's method table**, [`DelegateComponent`](../reference/traits/delegate_component.md) **is its vtable**, an **aggregate provider or namespace is a prototype** the object delegates to, and [`check_components!`](../reference/macros/check_components.md) **is the guarantee that the object responds to every message it will be sent** — `respond_to?` made total and moved to compile time. Reading a provider body, which sends messages to a context it never names, is reading duck-typed code. Framed this way, CGP is not a foreign paradigm to this reader but the mechanisms they already use — dispatch, vtables, delegation, duck typing — with the runtime taken out. + +The single expectation to correct, from which everything else follows, is that any of this happens at runtime. This reader will assume a vtable is a data structure the program carries, that dispatch chooses at the moment of the call, that a prototype chain is walked when a property is missing, and that the object graph can change while the program runs — and in CGP none of that is so. The table is erased after compilation, the dispatch is resolved by the type checker and inlined, the chain is walked during type resolution, and the wiring is fixed once the program is built. Present "late binding" honestly: in CGP the binding is late to the *wiring site* — the place a context declares its providers — not to runtime, so the flexibility is real but it is spent at compile time. The pitch that lands for this audience is the pair of things they most wish their own tools had: *duck typing that cannot blow up at runtime*, because a context missing a capability is a compile error rather than a 2 a.m. `NoMethodError`, and *dynamic dispatch that costs nothing*, because the polymorphism they rely on is resolved away instead of paid for on every call. For the prototype-minded reader specifically, the resonant framing is that CGP's delegation keeps `self` bound to the original context exactly as theirs keeps `this` bound to the receiver — same inheritance-by-delegation, but checked and free. + +The analogy to avoid is promising runtime behavior CGP does not have. A reader sold on heterogeneous collections, runtime plugins, or monkey-patching will feel misled the first time they reach for them and find the wiring frozen at compile time; say plainly that those live on the runtime side of the line, where Rust's `dyn Trait` and the dynamic languages themselves remain the right tools. Sell CGP as what it is — the static resolution of the mechanisms they know, trading runtime malleability for zero cost and compile-time safety — and the reader who has debugged a `this`-binding bug or chased a `NoMethodError` through a plugin will hear the trade as a good one. + +## Sources + +The account of the related work draws on standard references for dynamic dispatch and object models, the primary literature on prototype-based programming, and cited community writing for sentiment; the CGP snippets are drawn from the knowledge base's [aggregate providers](../concepts/aggregate-providers.md), [namespaces](../concepts/namespaces.md), and [consumer/provider](../concepts/consumer-and-provider-traits.md) material and verified against current macro behavior. + +- [*Dynamic dispatch* (Wikipedia)](https://en.wikipedia.org/wiki/Dynamic_dispatch) and [*Virtual method table* (Wikipedia)](https://en.wikipedia.org/wiki/Virtual_method_table) — late binding as runtime resolution of a call, Smalltalk's message-send `send`, single versus multiple dispatch, and the per-class vtable of function pointers reached through an object's vtable pointer. +- [*Duck typing* (Wikipedia)](https://en.wikipedia.org/wiki/Duck_typing) — an object's usability decided by the methods and properties it has rather than a declared class or interface. +- [geo-ant, *Rust Dyn Trait Objects and Fat Pointers*](https://geo-ant.github.io/blog/2023/rust-dyn-trait-objects-fat-pointers/) and [*Trait objects*, The Rust Programming Language Book](https://doc.rust-lang.org/book/ch18-02-trait-objects.html) — Rust's own dynamic dispatch as a data-pointer/vtable-pointer fat pointer, and the vtable's contents, the runtime counterpart to CGP's compile-time table. +- [Lieberman, *Using Prototypical Objects to Implement Shared Behavior in Object-Oriented Systems* (OOPSLA 1986)](https://web.media.mit.edu/~lieber/Lieberary/OOP/Delegation/Delegation.html) — delegation as forwarding unhandled messages to a prototype, and the defining rule that `self` stays bound to the original receiver, which distinguishes delegation from forwarding and matches CGP's context binding. +- [MDN, *Inheritance and the prototype chain*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain) — JavaScript's `[[Prototype]]` link, `Object.create`, prototype-chain lookup, and own-property shadowing, the runtime form of namespace inherit-and-override. +- [*Maps (Hidden Classes) in V8*](https://v8.dev/docs/hidden-classes) — hidden classes and inline caches descending from the Self language's maps and polymorphic inline caches, the engineering that dynamic dispatch's runtime cost demands. +- [SitePoint, *Making Ruby Quack — Why We Love Duck Typing*](https://www.sitepoint.com/making-ruby-quack-why-we-love-duck-typing/), [GeeksforGeeks, *Type Systems: Dynamic, Static & Duck Typing*](https://www.geeksforgeeks.org/python/type-systemsdynamic-typing-static-typing-duck-typing/), and [DevGex, *Duck Typing*](https://devgex.com/en/article/00035033) — community sentiment on what duck typing and dynamic typing buy (flexibility, brevity, rapid development) and cost (runtime errors, the `respond_to?` guard, refactoring hazards). diff --git a/docs/related-work/implicit-parameters.md b/docs/related-work/implicit-parameters.md new file mode 100644 index 00000000..2332afaf --- /dev/null +++ b/docs/related-work/implicit-parameters.md @@ -0,0 +1,165 @@ +# Implicit parameters + +Implicit parameters are function arguments the compiler supplies automatically from the surrounding context instead of the caller passing them by hand — Scala's `given`/`using` and Haskell's `ImplicitParams` are the direct forms, and the type-class resolution both languages build on is the same idea generalized. CGP shares the goal of threading context through code without explicit plumbing, but supplies the values from a context's fields and wiring rather than from a compiler-driven search, which trades away automatic resolution to gain the freedom to have many overlapping implementations. + +## Purpose + +Some values are needed everywhere and interesting nowhere: a configuration object, a logger, a comparison strategy, an error type, an execution environment. Passing them explicitly through every function that transitively needs them clutters signatures and forces intermediate functions to accept parameters they only forward. Implicit parameters remove that clutter by letting the caller omit such an argument and having the compiler fill it in from what is in scope, so the value propagates through a call chain without appearing at every call. The declaration still records the dependency in the type, but the *passing* becomes invisible. + +This is the same tension CGP resolves with [impl-side dependencies](../concepts/impl-side-dependencies.md) and [implicit arguments](../concepts/implicit-arguments.md), which is why the comparison is illuminating rather than incidental. Both CGP and the implicit-parameter languages want a piece of code deep in a call graph to obtain what it needs from its surroundings without every caller in between having to know. They differ in *what the surroundings are* and *how the value is found*: Scala and Haskell search an implicit scope by type, while CGP reads a field or resolves a wiring entry from the context that is already threaded through every provider. Showing a reader that CGP's context *is* their implicit environment — made a first-class, explicitly-wired type — is the heart of the comparison. + +## The concept in depth + +Implicit parameters appear in several forms that a reader may or may not distinguish, and the comparison to CGP is clearest when they are kept apart. There is the direct implicit *value* parameter (Scala's `using`, Haskell's `ImplicitParams`), and there is the type-class mechanism (Scala's `given` type classes, Haskell's `class`/`instance`) that is implicit resolution specialized to finding an implementation for a type. Both rest on the compiler searching a scope by type, and both are governed by *coherence*, the property that makes CGP's approach fundamentally different. + +### Context parameters in Scala (`using` and `given`) + +Scala 3 lets a parameter list be marked `using`, which makes its arguments contextual: at the call site the programmer may omit them, and the compiler supplies a matching `given` value from scope. A function that needs a `Config` everywhere in a rendering pipeline declares it once with `using` and never threads it again: + +```scala +def renderWebsite(path: String)(using config: Config): String = + "" + renderWidget(List("cart")) + "" // config passed implicitly + +def renderWidget(items: List[String])(using config: Config): String = ??? +``` + +A `given` value in scope is what the compiler injects for a `using` parameter, and the caller writes nothing: + +```scala +given Config = Config(8080, "docs.scala-lang.org") + +renderWebsite("/home") // the given Config is supplied automatically +``` + +The mechanism generalizes to type classes, which is its most common use. A `given` can be defined *for a type*, and a method that takes a `using` parameter of that type resolves the right instance by the types at the call site: + +```scala +trait Comparator[A]: + def compare(x: A, y: A): Int + +given Comparator[Int] with + def compare(x: Int, y: Int): Int = x - y + +def max[A](x: A, y: A)(using c: Comparator[A]): A = + if c.compare(x, y) > 0 then x else y + +max(1, 2) // Comparator[Int] resolved and passed implicitly +``` + +Scala uses this one mechanism for both dependency injection and type classes — a `using Config` is DI, a `using Comparator[A]` is a type class — which is why the language's implicits sit at the intersection of the two related-work topics; see also [dependency injection](dependency-injection.md). Historically this was all written with the `implicit` keyword in Scala 2; Scala 3 renamed it to `given`/`using` precisely because the old keyword was overloaded and hard to teach. + +### Implicit parameters in Haskell (`ImplicitParams`) + +Haskell's `ImplicitParams` extension is the more literal form of the same idea: a function can name a dynamically-bound variable `?x` of some type, which shows up as a constraint `(?x :: t)` on its signature and is filled from the binding in scope. A sort that needs a comparison declares it as an implicit parameter and calls it as `?cmp`: + +```haskell +sort :: (?cmp :: a -> a -> Bool) => [a] -> [a] +sort = sortBy ?cmp +``` + +The constraint propagates automatically to any caller that does not bind it, so a function built on `sort` inherits its `?cmp` without restating the intent: + +```haskell +least :: (?cmp :: a -> a -> Bool) => [a] -> a +least xs = head (sort xs) +``` + +The value is supplied with a `let` binding, at which point the constraint is discharged and the function below it becomes ordinary: + +```haskell +min :: Ord a => [a] -> a +min = let ?cmp = (<=) in least +``` + +`ImplicitParams` is the honest baseline for the comparison but a cautionary one: it is barely used in modern Haskell. The constraints leak into every signature, so the parameters are not very "implicit"; there is no way to declare a default; and the feature interacts awkwardly with the monomorphism restriction. Haskell programmers reach instead for the `Reader` monad to thread an environment, or, far more often, for type classes. + +### Type classes as implicit dictionary passing + +Type classes are the mechanism Haskell and Scala actually use for the implicit-resolution job, and understanding them as *implicit dictionary passing* is what connects them to CGP. A `class` declares an interface, an `instance` provides it for a type, and when a function constrained by the class is called, the compiler finds the instance for the concrete type and passes it as a hidden argument — a *dictionary* of the instance's methods. The constraint `Ord a =>` is elaborated into an extra parameter carrying the `Ord` dictionary: + +```haskell +class Ord a where + compare :: a -> a -> Ordering + +instance Ord Int where + compare x y = ... -- the compiler builds and passes an Ord Int dictionary +``` + +The resolution is *type-directed and automatic*: the programmer names the constraint, and the compiler searches for the matching instance by type, with no explicit selection. This is exactly the convenience CGP gives up and the reason it can do something type classes cannot, so the next point is the pivot of the whole comparison. + +### Coherence: one instance, globally + +The property that governs type-class and implicit resolution — and that CGP deliberately abandons — is *coherence*: for a given type there is exactly one instance, and every resolution anywhere in the program finds the same one. Coherence is what makes automatic resolution safe. Because the compiler will always find the same `Ord Int`, it can inject it silently without the programmer worrying that a different `Ord Int` might be chosen elsewhere and make two pieces of code disagree. Haskell enforces this with the orphan-instance rule and by forbidding overlapping instances; Scala's implicits are coherent in the common case and rely on scoping and priority rules at the edges. + +The price of coherence is the price CGP was built to escape. Because there can be only one `Ord Int`, a program cannot have two legitimate orderings of `Int` as first-class instances — the standard workaround is to wrap the type in a `newtype` so a second instance attaches to a distinct type. And a crate cannot add an instance for a type and class it does not own. These are the same overlap and orphan restrictions Rust's own coherence imposes, described from the CGP side in [bypassing coherence](../concepts/coherence.md). The comparison to CGP therefore comes down to a single trade: implicit resolution buys automatic, type-directed selection at the cost of one global instance per type, while CGP buys many overlapping per-context implementations at the cost of selecting them explicitly. + +## How CGP expresses it + +CGP threads context through code and lets deep code read what it needs, but it does so through the context that is already the `Self` of every provider, not through a scope search. The value a Scala `using` parameter or a Haskell implicit parameter carries is, in CGP, a field of the context or an entry in its wiring — and the context is passed to every provider automatically as the receiver, so nothing has to be threaded by hand. Two CGP constructs map onto the two forms of implicit parameter: [`#[implicit]`](../reference/attributes/implicit.md) arguments correspond to implicit *value* parameters, and the [consumer/provider component](../concepts/consumer-and-provider-traits.md) with its wiring corresponds to type classes. + +### Implicit arguments are implicit value parameters + +An `#[implicit]` argument is written as an ordinary function parameter but is supplied from the context's fields rather than by the caller, which is precisely the shape of a Scala `using` parameter — except the source is a struct field, not a `given` in scope. A provider that needs width and height reads them as implicit arguments, and any context carrying those fields supplies them: + +```rust +#[cgp_fn] +pub fn rectangle_area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 { + width * height +} +``` + +Where Scala's `renderWebsite` obtains its `Config` from an implicit `given`, a CGP provider obtains `width` and `height` from the context threaded through it as `self`. The parallel is close enough that CGP's own name for the feature — *implicit arguments* — is the same word Scala and Haskell use, and the desugaring is the same idea: the parameter disappears from the public signature and is bound from the surroundings before the body runs. The difference is where "the surroundings" live. A `using` parameter searches the implicit scope by type; an `#[implicit]` argument reads the context field of the matching name, so resolution is by *field*, decided when the context is defined, not by a compiler search at the call site. + +### A shared context value is a threaded environment + +When several pieces of code must agree on one value — the case Scala handles by a single `given Config` in scope and Haskell by the `Reader` monad — CGP has them all read the same field or the same [abstract type](../concepts/abstract-types.md) from the shared context. A configuration value read by many providers is injected once and every provider that names it receives the same one, with no coordination between them, exactly as one `given Config` serves an entire Scala call graph. CGP's shared error type is the canonical instance: every fallible provider names the context's `Self::Error`, so they all agree on one error type the context supplies once — the type-level echo of a threaded `Reader` environment, resolved at compile time and paid for at runtime with nothing. + +### Components and wiring are type classes without coherence + +A CGP [component](../concepts/consumer-and-provider-traits.md) is a type class, a provider is an instance, and wiring is the resolution step — but because a provider's `Self` is its own marker type, many providers for the same component coexist without violating coherence, which is what type-class instances cannot do. The `Comparator[Int]` that Scala can define only once, or the `Ord Int` that Haskell pins globally, becomes in CGP any number of interchangeable providers, each selected per context. The [modular serialization](../examples/modular-serialization.md) example makes the contrast concrete: `UseSerde`, `SerializeBytes`, and `SerializeWithDisplay` all serialize a `String` and overlap freely, where the equivalent overlapping type-class instances would be rejected: + +```rust +#[cgp_impl(UseSerde)] +impl ValueSerializer +where + Value: serde::Serialize, +{ /* ... */ } + +#[cgp_impl(SerializeBytes)] +impl ValueSerializer +where + Value: AsRef<[u8]>, +{ /* ... */ } +``` + +The cost of this freedom is the flip side of the type-class trade: CGP will not *find* the provider for you by type. A context names its choice in a [`delegate_components!`](../reference/macros/delegate_components.md) table, where Scala and Haskell would resolve the instance automatically from the type. What CGP loses in automatic resolution it gains in the ability to have `AppA` serialize a `Vec` as hexadecimal and `AppB` as base64 — two coherent local choices where a global instance would force one answer on both. + +## What users like and dislike + +Implicit parameters and the type classes built on them are among the most loved and most criticized features of the languages that have them, and both reactions are instructive. What users value is the erasure of boilerplate: a `Config`, a comparator, or an execution context threads through a deep call graph without appearing at every call, and type classes let one generic function work over any type that has an instance, resolved automatically. Scala programmers build whole architectures on implicits — type-class derivation, context propagation, dependency injection — precisely because the mechanism is so general, and Haskell's type classes are widely regarded as one of the cleanest solutions to ad-hoc polymorphism in any language. + +The dislike is the mirror image of the same generality: implicit resolution is hard to follow. The recurring Scala complaint is that a value appears "from nowhere," and tracking down *which* `given` was selected — or why an expected one was not — is a notorious time sink, made worse in Scala 2 by the single overloaded `implicit` keyword that Scala 3 split apart to address. Error messages when resolution fails are often opaque. Haskell's `ImplicitParams` is disliked enough to be effectively abandoned, for the concrete reasons that the constraints leak into every signature, no default can be given, and the monomorphism restriction interferes. And coherence itself, though it is what makes resolution safe, is a persistent source of friction: the orphan rule forces awkward module structure, and the one-instance-per-type limit forces `newtype` wrappers whenever a second interpretation of a type is wanted. + +## How CGP compares + +CGP makes the opposite trade from implicit resolution on the two axes that matter, and the comparison is cleanest stated as that trade. On *resolution*, implicit parameters are automatic and type-directed while CGP wiring is explicit: Scala and Haskell find the instance for you, and CGP asks you to name it in a table. On *coherence*, implicit parameters are coherent while CGP is not: the languages guarantee one instance per type and forbid overlap and orphans, and CGP permits unlimited overlapping providers and per-context choice by moving `Self` to a provider marker. Each side pays for what the other gets. The implicit-parameter languages get zero-boilerplate resolution and pay with the one-instance restriction and the "where did this come from" opacity; CGP gets many local implementations and explicit, greppable wiring and pays with the wiring itself — there is no automatic search, so the choice must be written down. + +Neither trade is strictly better, and the honest positioning names where each wins. When a program genuinely wants one canonical instance per type — one `Ord`, one `Show`, one serialization — and values automatic resolution above all, type classes are the better tool, and fighting their coherence with CGP-style machinery would be over-engineering. When a program needs several interchangeable implementations, per-deployment or per-context choice, or must implement a behavior for types and traits it does not own, CGP's explicit wiring is the better tool, and the coherence it discards was the very thing in the way. CGP's resolution being explicit is also, for its intended audience, a feature rather than a cost: the wiring table is the one place selection is decided, so the "spooky" resolution that dogs implicits is replaced by a lookup a reader can point at. + +## Presenting CGP to someone who knows this + +A reader fluent in implicits or type classes holds most of CGP's conceptual furniture already, and the move that unlocks it is to name the correspondence outright: a **component is a type class**, a **provider is an instance**, **wiring is instance resolution**, and an **`#[implicit]` argument is a `using` parameter** whose value comes from a context field. The context itself is the implicit environment they already reason about — the `Reader` they thread, the set of `given`s in scope — reified as a single explicit type that every provider receives automatically. Framed this way, CGP is not a foreign paradigm but their own implicit machinery with the resolution step made visible. + +The one thing to correct up front is the expectation of automatic resolution. This reader will assume CGP finds the provider by type the way their compiler finds an instance, and it does not — wiring is written by hand in a table. Present that not as a missing feature but as the deliberate consequence of the capability they will find most striking: because CGP does not resolve by type, it is free of coherence, so it can host the overlapping instances their language forbids. Lead with the pain coherence causes them — the `newtype` dance to get a second `Ord`, the orphan-rule contortions to add an instance for a foreign type, the single global choice forced on unrelated code — and show that each disappears when providers are distinct marker types selected per context. For the Scala reader specifically, the resonant pitch is "implicits without the mystery": the value still arrives without being threaded by hand, but *which* implementation was chosen is a line in a wiring table rather than the outcome of a scope search, so the debugging nightmare they know is designed out. For the Haskell reader, the pitch is "type classes without the orphan rule, and overlapping instances made legal." + +The analogy to avoid is promising that CGP "just finds the right implementation." It does not, and a reader sold on automatic resolution will feel misled the first time they have to write a `delegate_components!` entry. Set the expectation honestly — you name the provider, once, per context — and pair it immediately with what that explicitness buys: no ambiguity, no hidden priority rules, no coherence straitjacket, and the same implementation-hiding decoupling delivered through a table they can read. A reader who has spent an afternoon debugging an implicit resolution will hear the trade as a good one. + +## Sources + +The account of the related work draws on the official language documentation, primary references on type-class semantics, and community writing on the ergonomics of implicits, cited where a specific claim rests on one. + +- [Scala 3 Book — Context Parameters](https://docs.scala-lang.org/scala3/book/ca-context-parameters.html) and [Contextual Parameters (Tour of Scala)](https://docs.scala-lang.org/tour/implicit-parameters.html) — the authoritative description of `using` clauses, `given` instances, and type-directed resolution. +- [Scala 3 Implicit Redesign (Baeldung)](https://www.baeldung.com/scala/scala-3-implicit-redesign) — the rename from Scala 2's overloaded `implicit` keyword to `given`/`using` and the reasons for it. +- [GHC User's Guide — Implicit Parameters](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/implicit_parameters.html) and [Implicit parameters (HaskellWiki)](https://wiki.haskell.org/Implicit_parameters) — the `?x` constraint syntax, `let`-binding, propagation semantics, and the noted limitations that keep the feature little-used. +- [Type class (Wikipedia)](https://en.wikipedia.org/wiki/Type_class) and [Implementing, and Understanding Type Classes (okmij.org)](https://okmij.org/ftp/Computation/typeclass.html) — type classes as dictionary-passing elaboration and how instances are resolved. +- [Type classes: confluence, coherence and global uniqueness (ezyang's blog)](https://blog.ezyang.com/2014/07/type-classes-confluence-coherence-global-uniqueness/) and [Coherence of Type Class Resolution (Bottu et al.)](https://xnning.github.io/papers/coherence-class.pdf) — the definition of coherence and why it constrains instances to one per type. diff --git a/docs/related-work/ml-modules.md b/docs/related-work/ml-modules.md new file mode 100644 index 00000000..7f02c5ae --- /dev/null +++ b/docs/related-work/ml-modules.md @@ -0,0 +1,189 @@ +# ML modules and modular implicits + +ML modules are the signature/structure/functor system of OCaml and Standard ML — interfaces that specify types and values, implementations that satisfy them, and functors that turn one module into another — and *modular implicits* is the OCaml extension that adds type-directed, type-class-style resolution on top of that system. CGP's components, providers, and higher-order providers line up closely with signatures, structures, and functors, but CGP replaces the manual, ordered functor application that assembling a large ML program requires with a declarative wiring table that the compiler resolves — landing somewhere between raw functors and modular implicits, and adding per-context selection that neither of them has. + +## Purpose + +ML modules solve the problem of building large programs out of interchangeable, separately-specified parts with genuine type abstraction. A *signature* states what a component provides — some abstract types and some operations over them — without fixing their representation; a *structure* implements a signature; and a *functor* is a module parameterized by another module, so a component can be written once against an interface and instantiated many ways. This is the ML answer to modularity, dependency injection, and data abstraction all at once, and it is widely regarded as one of the most expressive module systems any language offers ([Dreyer, *Understanding and Evolving the ML Module System*](https://people.mpi-sws.org/~dreyer/thesis/main.pdf)). + +This is the same territory CGP occupies, which is why the comparison is close rather than incidental. A CGP [component](../reference/macros/cgp_component.md) is an interface, a [provider](../reference/macros/cgp_impl.md) is an implementation, a [higher-order provider](../concepts/higher-order-providers.md) is a parameterized implementation, and [abstract-type components](../concepts/abstract-types.md) are exactly the abstract types a signature declares. The two paradigms diverge on *how a program is assembled from these parts* and *how the parts are selected*: ML makes you apply functors by hand in dependency order and select modules by name, whereas CGP resolves the dependency graph through the trait system from a declarative table keyed on the context. Modular implicits sits between them — it keeps ML's modules but adds automatic, type-directed selection — so the honest comparison runs across all three. It also connects to the [dependency-injection](dependency-injection.md), [implicit-parameters](implicit-parameters.md), and [algebraic-effects](algebraic-effects.md) documents, because ML modules, type classes, implicits, and CGP are one family of answers to the same question of how to choose an implementation. + +## The concept in depth + +ML modules layer three constructs — signatures, structures, and functors — on top of the core language, add abstract types and the sharing constraints that reconcile them, and distinguish applicative from generative functor application; modular implicits then adds implicit resolution over the whole thing. The subsections build up in that order, using OCaml syntax throughout, and close on the theoretical result that ties modules to type classes and thereby to CGP. + +### Signatures, structures, and abstract types + +A *signature* is an interface that specifies types and values, and a *structure* is an implementation that satisfies it, with a signature's *abstract types* giving true data abstraction. A signature that specifies an abstract type `t` and a `compare` over it, and a structure implementing it, read as: + +```ocaml +module type OrderedType = sig + type t + val compare : t -> t -> int +end + +module StringCompare = struct + type t = string + let compare = String.compare +end +``` + +The `type t` in the signature, left without a definition, is the crux of ML data abstraction: when a structure is *sealed* with a signature that keeps `t` abstract, clients cannot see the representation, so the module is free to change it, and the type system enforces representation independence ([*Modules and Data Abstraction in OCaml*](https://cs.wellesley.edu/~cs251/s12/handouts/modules.pdf)). This *sealing* is a stronger form of abstraction than mere genericity — it hides a *known* type's representation, not just abstracts over an unknown one. + +### Functors: modules parameterized by modules + +A *functor* is a function from modules to modules, and it is how ML expresses a reusable, parameterized component. A functor takes a module of some signature and produces a module built on top of it: + +```ocaml +module Make (O : OrderedType) = struct + (* ... a set implementation using O.compare ... *) +end + +module StringSet = Make (StringCompare) +``` + +The application `Make (StringCompare)` is explicit and by name: the programmer chooses the argument module and writes the application. Functors are the ML mechanism for dependency injection at the module level — `Make` depends on *some* ordered type without naming which — and OCaml's standard library uses exactly this shape, `Set.Make(String)`, throughout ([OCaml, *Functors*](https://ocaml.org/docs/functors)). + +### Sharing constraints and the applicative/generative distinction + +Two subtleties make functors harder to use than the shape above suggests: sharing constraints and the applicative/generative choice. When a functor result's abstract type must be known to equal some other type, the programmer writes a *sharing constraint* with `with type`, as in `Make (O) : S with type t = O.t` — a construct widely found confusing and a recurring source of type errors. Separately, applying a functor twice raises the question of whether the two results' abstract types are equal: OCaml functors are *applicative* by default, so `Make(X).t` equals `Make(X).t` for the same `X`, while SML functors are *generative*, minting fresh incompatible types on each application; OCaml marks a functor generative by giving it a final `()` argument ([OCaml manual, *First-class modules*](https://ocaml.org/manual/5.4/firstclassmodules.html); [practical example thread](https://discuss.ocaml.org/t/practical-example-of-applicative-vs-generative-functors/13777)). These are the fine-grained type-equality controls that make ML modules powerful and also make them heavy. + +### Assembling a program: manual functor plumbing + +Building a whole application from functors means applying each one by hand, in dependency order, and this plumbing is a documented pain point at scale. Because a functor argument must be supplied explicitly, a large program becomes a sequence of functor applications — `module A = MakeA (...)`, `module B = MakeB (A)`, `module C = MakeC (A) (B)`, and so on — that the programmer writes out and orders correctly. The MirageOS project makes the cost concrete: a unikernel there can reach a *functor application depth of up to 10* across *more than 70 modules*, and the community built an entire DSL, **Functoria**, whose sole purpose is to *organize functor applications*, because OCaml's module language "is much less flexible than its expression language" and cannot express the conditional, dependency-driven wiring such assembly needs ([*Programming Unikernels in the Large via Functor Driven Development*](https://arxiv.org/pdf/1905.02529)). That a mature ecosystem wrote a configuration DSL to escape manual functor application is the sharpest evidence of the limitation CGP's wiring addresses. + +### Modular implicits: type-directed resolution over modules + +*Modular implicits* extends OCaml so that module arguments can be marked implicit and resolved automatically by type, bringing type-class-style ad-hoc polymorphism to the module system. Introduced by Leo White, Frédéric Bour, and Jeremy Yallop, it lets a function take an implicit module parameter, written in curly braces, and lets the caller omit it; the compiler searches the implicit modules in scope for one of the required signature ([White, Bour & Yallop, *Modular implicits*](https://arxiv.org/abs/1512.01895)): + +```ocaml +module type SERIALIZABLE = sig + type t + val to_string : t -> string +end + +let to_string (type a) {S : SERIALIZABLE with type t = a} = S.to_string + +implicit module SInt : SERIALIZABLE with type t = int = struct + type t = int + let to_string = string_of_int +end + +implicit module SList {A : SERIALIZABLE} : SERIALIZABLE with type t = A.t list = struct + type t = A.t list + let to_string l = "[" ^ String.concat ";" (List.map A.to_string l) ^ "]" +end +``` + +At a call site, `to_string 2` resolves the implicit to `SInt`, and `to_string [2; 3]` resolves it to the *implicit functor* `SList` applied to `SInt` — recursive resolution that constructs the needed module by applying implicit functors, the module-system counterpart of Haskell resolving `Show [Int]` from `Show Int` ([tycon, *First-Class Modules and Modular Implicits*](https://tycon.github.io/modular-implicits.html)). It is motivated by OCaml's lack of ad-hoc polymorphism — the notorious separate `+` and `+.`, `print_int` versus `print_string` — and elaborates into OCaml's existing first-class functors, so it adds no new runtime mechanism. It remains an [experimental fork](https://github.com/ocamllabs/ocaml-modular-implicits), never merged into mainline OCaml. + +### Classes as signatures: the modular-type-classes result + +The mapping this comparison rests on — component to signature, provider to structure, higher-order provider to functor — is a known theoretical result, not an improvisation. Dreyer, Harper, and Chakravarty's *Modular Type Classes* treats **classes as signatures and instances as structures and functors**, which is exactly the correspondence CGP reuses ([Dreyer, Harper & Chakravarty, *Modular Type Classes*](https://people.mpi-sws.org/~dreyer/papers/mtc/main-long.pdf)). That result belongs as much to the [type classes](type-classes.md) comparison as to this one, and is developed there in full — including the *canonicity*-versus-modularity tension it exposes and how CGP resolves it by abandoning canonicity for per-context selection, as [bypassing coherence](../concepts/coherence.md) describes. Here it is enough that the module side of the mapping has a firm footing: a CGP component really can be read as a signature and a provider as a structure or functor. + +## How CGP expresses it + +CGP reproduces the signature/structure/functor triad with its consumer/provider machinery, then replaces manual functor application with a declarative table. The correspondence is close enough to translate construct by construct, and the two genuine differences — declarative wiring in place of ordered functor application, and per-context selection in place of by-name linking or implicit search — are exactly where CGP earns its keep. + +### Components are signatures; providers are structures + +A CGP component is a signature and a provider is a structure implementing it, matching the modular-type-classes mapping directly. Declaring a component states the interface, and a provider supplies the implementation: + +```rust +#[cgp_component(AreaCalculator)] +pub trait CanCalculateArea { + fn area(&self) -> f64; +} + +#[cgp_impl(new RectangleArea)] +impl AreaCalculator { + fn area(&self, #[implicit] width: f64, #[implicit] height: f64) -> f64 { + width * height + } +} +``` + +`CanCalculateArea` is the signature — the operations a caller may invoke — and `RectangleArea` is a structure ascribing to it. The one piece with no ML counterpart is CGP's [consumer/provider duality](../concepts/consumer-and-provider-traits.md): CGP splits the trait into a consumer side callers use and a provider side implementers target, whereas an ML signature is a single interface used from both sides. That split exists because Rust has coherence and ML does not — ML lets a program hold many structures of one signature simply by *naming them*, so it never needs the maneuver CGP uses to escape the one-implementation-per-type rule. A CGP component is also typically a *small* signature — one capability — where an ML signature bundles many values and types; a rich ML signature corresponds to a bundle of CGP components combined through supertraits and [`#[uses]`](../reference/attributes/uses.md). + +### Abstract-type components are a signature's abstract types + +An [abstract-type component](../concepts/abstract-types.md) is CGP's version of a signature's abstract `type t`, defined with [`#[cgp_type]`](../reference/macros/cgp_type.md): + +```rust +#[cgp_type] +pub trait HasErrorType { + type Error: Debug; +} +``` + +`HasErrorType` declares an abstract `Error` the way `OrderedType` declares an abstract `t`, and generic code refers to it as `Self::Error` without naming a concrete type, just as functor bodies refer to `O.t`. The abstraction here is the *parametric* kind — generic code is polymorphic over the type it is given — which matches how a functor body abstracts over its argument's types. What CGP does *not* reproduce is ML's *sealing*: once a context wires `Error` to `anyhow::Error`, there is no type-system boundary hiding that representation from other code, so CGP leaves representation-hiding to Rust's ordinary module privacy rather than to the abstract-type mechanism. CGP abstract types defer and configure a type; ML abstract types can additionally seal one. + +### Higher-order providers are functors + +A [higher-order provider](../concepts/higher-order-providers.md) is a functor: a provider parameterized by another provider, exactly as a functor is a module parameterized by another module. A `ScaledArea` provider takes an inner calculator and builds on it: + +```rust +#[cgp_impl(new ScaledArea)] +#[use_provider(InnerCalculator: AreaCalculator)] +impl AreaCalculator { + fn area(&self, #[implicit] scale_factor: f64) -> f64 { + InnerCalculator::area(self) * scale_factor * scale_factor + } +} +``` + +`ScaledArea` is `Make (StringCompare)` in CGP form — a parameterized implementation applied to a specific argument. Two differences distinguish it from a plain functor, and both favor CGP's ergonomics. First, a higher-order provider can default its inner parameter to [`UseContext`](../reference/providers/use_context.md), so an unparameterized `ScaledArea` falls back to whatever the context itself wires for that component — an *open, recursive* application that a plain functor, which always demands its argument explicitly, cannot express without recursive modules. Second, CGP sidesteps the sharing-constraint problem entirely: where an ML functor needs `with type t = O.t` to make its result's abstract types line up with its argument's, CGP's abstract types are supplied by the *shared context* through `HasType`, so every provider in a context automatically agrees on `Self::Error` and `Self::Scalar` with no sharing annotation to write. + +### `delegate_components!` replaces manual functor application + +The decisive difference is that CGP wires a program declaratively where ML applies functors by hand in order. A context lists its component-to-provider choices in a [`delegate_components!`](../reference/macros/delegate_components.md) table, in any order, and the trait system resolves each provider's dependencies through the context: + +```rust +delegate_components! { + App { + AreaCalculatorComponent: ScaledArea, + ErrorTypeProviderComponent: UseType, + // ... further components, in any order + } +} +``` + +This is CGP's built-in Functoria. Where MirageOS needed a separate DSL to organize functor applications up to ten deep, CGP resolves that dependency graph as ordinary trait resolution: a provider states what it needs as [impl-side dependencies](../concepts/impl-side-dependencies.md), and the compiler threads each requirement to whatever provider the context wires for it, with no application order to get right. The counterpart to ML's type-checking that a functor argument satisfies its parameter signature is [`check_components!`](../reference/macros/check_components.md), which verifies at compile time that a context supplies every capability its providers transitively need. Grouping a reusable bundle of wirings is an [aggregate provider](../concepts/aggregate-providers.md) or a [namespace](../concepts/namespaces.md) — CGP's way of packaging a sub-assembly the way a functor packages a parameterized structure. + +### Modular implicits and CGP wiring: the same problem, opposite selection + +Modular implicits and `delegate_components!` are two answers to the same limitation of raw ML modules — that assembling a program from functors is manual — but they select implementations by opposite means. Modular implicits resolves by *implicit search*: the compiler looks through the implicit modules and functors in scope for one matching the required signature, constructing it recursively, which is the type-class route and shares type classes' coherence-leaning character (a search can be ambiguous, and canonicity fights abstraction). CGP resolves by *explicit table*: a context names one provider per component, so there is no search, no ambiguity, and each context may choose differently — the per-context selection [coherence](../concepts/coherence.md) is built to allow. The deep mechanism the two share is *recursive composition*: an implicit functor like `SList {A : SERIALIZABLE}` builds `Show` of a list from `Show` of its element, exactly as a higher-order provider builds its behavior from an inner provider resolved through the context. So the honest statement is not that modular implicits *is* `delegate_components!` but that both automate the functor plumbing ML leaves manual, one by searching and one by tabulating, over the same recursive-composition core. + +## What users like and dislike + +ML modules are admired for exactly the abstraction and parameterization they were designed to provide, and the praise is long-standing. Practitioners value *genuine data abstraction* — sealing hides a representation and the type system enforces it — *functors* for writing reusable components against an interface, *separate compilation*, and *first-class modules* for the cases where an implementation must be chosen at runtime ([OCaml manual, *First-class modules*](https://ocaml.org/manual/5.4/firstclassmodules.html)). The module system is routinely described as one of the most expressive and principled in any language, and the theoretical work around it — applicative functors, 1ML, modular type classes — is a rich, respected literature. + +The complaints cluster around weight, plumbing, and the absence of implicit resolution. The most concrete is *functor boilerplate at scale*: assembling a large application means applying functors by hand in dependency order, painful enough that MirageOS built Functoria to manage it, and rooted in the *stratification* of ML — the module language is a second, weaker language layered above the expression language, without the conditionals and flexible dependency handling ordinary code enjoys ([*Functor Driven Development*](https://arxiv.org/pdf/1905.02529); [Rossberg, *1ML — core and modules united*](https://www.cambridge.org/core/journals/journal-of-functional-programming/article/1ml-core-and-modules-united/47B10882829E4B32F98FBA93B28CEF30)). *Sharing constraints* (`with type`) are a recurring source of confusion, and the *applicative/generative* distinction is a subtlety that trips people. Above all, base ML has *no ad-hoc polymorphism*: everything must be named and applied explicitly, the very gap that motivated modular implicits, whose paper opens on OCaml's separate `+` and `+.` and its family of `print_*` functions ([White, Bour & Yallop 2015](https://arxiv.org/abs/1512.01895)). Modular implicits itself is liked for bringing type-class ergonomics to OCaml by reusing the module system rather than bolting on a separate class construct, and for naturally supporting inheritance, constructor classes, and associated types — but it is disliked, or at least unavailable, for the plainest of reasons: it remains an experimental fork and has never landed in mainline OCaml, and its own authors note that canonicity cannot be fully reconciled with modular abstraction. + +## How CGP compares + +CGP keeps the ML module correspondence — component is signature, provider is structure, higher-order provider is functor — and changes the two things ML users complain about, at the cost of the one thing ML does best. On *assembly*, CGP is declarative where ML is manual: a `delegate_components!` table lists choices in any order and the compiler resolves the dependency graph, so there is no functor-application chain to write or order, and no need for a Functoria-style DSL because CGP's wiring already is one. On *selection*, CGP is per-context and coherence-free where ML is by-name-explicit and modular implicits is implicit-search: two contexts wire the same component to different providers with no conflict, no ambiguity, and no canonicity constraint. And on *stratification*, CGP has none — providers and contexts are ordinary Rust types and traits, wired in ordinary Rust, so there is no separate, weaker module language above the expression language. What CGP gives up is ML's *sealing*: CGP's abstract types defer and configure a type but do not hide a representation behind a type-system boundary, so representation hiding falls to Rust's module privacy rather than to the abstraction mechanism itself. + +The costs beyond sealing are worth naming plainly. ML modules are separately compiled and their functor applications are simple, legible module expressions; CGP's wiring is trait resolution, which compiles to zero-overhead direct calls but produces the verbose, generated-type errors the [check traits](../concepts/check-traits.md) exist to tame. ML's first-class modules let an implementation be chosen at runtime from a value; CGP resolves at compile time and monomorphizes, so a genuinely runtime-chosen implementation needs a different tool. And ML's explicit, by-name linking gives a control and predictability that some programs want, where CGP's table-driven resolution is more automatic but less locally obvious. + +Neither is uniformly better, and the honest positioning names where each wins. When a program needs true representation hiding enforced by the type system, separately-compiled modules, runtime selection via first-class modules, or the explicit control of by-name linking, ML modules are the better tool, and reaching for CGP would forgo abstraction guarantees ML provides natively. When a program needs many interchangeable implementations chosen per deployment, a dependency graph the compiler wires rather than the programmer, abstract types unified into the same selection mechanism, and all of it in one language with no module stratum — and can accept Rust as the platform — CGP delivers the functor-and-signature style of modularity without the manual plumbing, and does so as a working library where modular implicits, the ML feature aimed at the same ergonomics, is still an unmerged fork. + +## Presenting CGP to someone who knows this + +A reader fluent in ML modules holds most of CGP's structure already, and the fastest way in is to translate the vocabulary and then flag the two changes. A **component is a signature**, a **provider is a structure**, a **higher-order provider is a functor**, an **abstract-type component is a signature's abstract `type t`**, and **`delegate_components!` is the functor-application configuration** — the thing they would otherwise write by hand or reach for Functoria to manage. **`check_components!`** is the check that a module satisfies the signature its consumer requires, moved to the wiring site. Framed this way, CGP is not a foreign paradigm but the signature/structure/functor style they know, with the assembly step turned from a manual functor chain into a declarative table. + +Two expectations to correct up front, because an ML reader will carry both. The first is *application order*: this reader expects to apply functors themselves, in dependency order, and CGP does not work that way — the table is unordered and the compiler resolves each provider's dependencies through the context, so the "wire A before B before C" discipline simply does not exist. Present that as the payoff, not a loss of control: it is Functoria's job done by the type system. The second is *abstraction*: this reader expects an abstract type to be *sealed*, its representation hidden by the signature, and CGP's abstract types are not sealed — they defer and configure a type but do not hide a known one, so representation hiding is Rust's module privacy, separate from the abstract-type mechanism. Say this plainly, because a reader who assumes sealing will look for a guarantee CGP does not make through this feature. + +The analogy to reach for, especially with an OCaml reader who has felt the pain, is *functors with the plumbing automated*. If they have wired a MirageOS unikernel or wished for modular implicits, the pitch lands: CGP is the functor-and-signature discipline with declarative, compiler-resolved wiring, per-context selection instead of a canonical-instance search, and no separate module language — and unlike modular implicits, it is not an experimental fork but a library on stable Rust. For the reader who knows *Modular Type Classes*, the sharpest framing is that CGP takes that paper's mapping — classes as signatures, instances as structures and functors — and makes the *explicit-linking* default the whole design, dropping canonicity in favor of a per-context table, which is precisely the choice that lets it host the overlapping implementations coherence would forbid. + +## Sources + +The account of the related work draws on the OCaml documentation, the primary literature on ML modules and their relationship to type classes, and the modular-implicits proposal and its implementation; the CGP snippets are drawn from the knowledge base's [consumer/provider](../concepts/consumer-and-provider-traits.md), [abstract types](../concepts/abstract-types.md), and [higher-order providers](../concepts/higher-order-providers.md) documents and verified against current macro behavior. + +- [OCaml — *Functors*](https://ocaml.org/docs/functors) and [OCaml manual — *First-class modules*](https://ocaml.org/manual/5.4/firstclassmodules.html) — the concrete syntax of signatures, structures, functors, functor application, `with type` sharing constraints, and the applicative/generative distinction. +- [*Modules and Data Abstraction in OCaml* (Wellesley CS251)](https://cs.wellesley.edu/~cs251/s12/handouts/modules.pdf) and [*A Crash Course on ML Modules*](https://jozefg.bitbucket.io/posts/2015-01-08-modules.html) — signatures as interfaces, structures as implementations, and sealing for data abstraction. +- [Dreyer, *Understanding and Evolving the ML Module System* (PhD thesis)](https://people.mpi-sws.org/~dreyer/thesis/main.pdf) and [Rossberg, *1ML — core and modules united*](https://www.cambridge.org/core/journals/journal-of-functional-programming/article/1ml-core-and-modules-united/47B10882829E4B32F98FBA93B28CEF30) — the depth of the ML module system and the critique of its stratification from the expression language. +- [Dreyer, Harper & Chakravarty, *Modular Type Classes* (POPL 2007)](https://people.mpi-sws.org/~dreyer/papers/mtc/main-long.pdf) — classes as signatures, instances as structures and functors, canonical instances for implicit resolution, and the canonicity-versus-abstraction tension. +- [White, Bour & Yallop, *Modular implicits* (ML Workshop 2014)](https://arxiv.org/abs/1512.01895) ([Cambridge PDF](https://www.cl.cam.ac.uk/~jdy22/papers/modular-implicits.pdf)), the [experimental fork](https://github.com/ocamllabs/ocaml-modular-implicits), and [tycon, *First-Class Modules and Modular Implicits in OCaml*](https://tycon.github.io/modular-implicits.html) — implicit module parameters, implicit functors, recursive resolution, and the ad-hoc-polymorphism motivation. +- [*Programming Unikernels in the Large via Functor Driven Development* (MirageOS / Functoria)](https://arxiv.org/pdf/1905.02529) — the functor-application boilerplate at scale (depth up to 10, 70+ modules) and the DSL built to manage it. +- [Applicative vs. generative functors (OCaml discussion)](https://discuss.ocaml.org/t/practical-example-of-applicative-vs-generative-functors/13777) — the practical distinction and its type-equality consequences, tracing to Leroy's module design. diff --git a/docs/related-work/reflection.md b/docs/related-work/reflection.md new file mode 100644 index 00000000..d9869358 --- /dev/null +++ b/docs/related-work/reflection.md @@ -0,0 +1,249 @@ +# Reflection and compile-time introspection + +Reflection is the ability of a program to inspect a type's own structure — its fields, its variants, their names and types — and act on it generically, whether that inspection happens at runtime through a type descriptor a framework walks (Bevy, Go, Java) or at compile time through introspection the compiler evaluates away (Zig's `comptime`, C++26, D, and the emerging Rust nightly reflection). CGP reaches the same destination as compile-time reflection — one generic routine that works over any type's fields — but takes the structure not as *data a routine inspects* but as *types the trait system resolves against*, so its "reflection" is encoded in the type system, checked when the generic code is written rather than when it is instantiated, and erased before the program runs. + +## Purpose + +Reflection solves the problem of writing one piece of code that works over the *shape* of a type it was not written for. A serializer, a dependency-injection container, an ORM, a scene editor, a configuration loader, a debugger's pretty-printer — each needs to walk an arbitrary type's fields by name and type without the type's author having hand-written support for it. Reflection supplies that by making a type's structure available as something the program can read: a `reflect.Type` value in Go, a `Class` in Java, a `TypeInfo` in Bevy, a `std.builtin.Type` in Zig, a `std::meta::info` in C++26, a `core::mem::Type` in nightly Rust. Generic code then loops over that description — over the fields, the variants, the methods — and does its job for any type at once, instead of the type's author writing the same boilerplate again and again. + +This is the same payoff CGP's [extensible-data machinery](../concepts/extensible-records.md) delivers, which is why the comparison is foundational rather than incidental: CGP's own account of extensible records says the derive "brings the row-polymorphism of languages like PureScript and the structural typing of records to Rust," and structural, name-driven access to a type's fields is exactly what reflection provides. Where they diverge is *what the structure is made into* and *when the access is checked*. A reflection facility turns the structure into a value — a descriptor a routine inspects at runtime or in a `comptime` block; CGP turns it into a *type* — a type-level list a generic routine resolves against through trait recursion, checked by the compiler at the definition site. This document meets the reader who thinks in `TypeInfo`, `@typeInfo`, and `#[derive(Reflect)]`, gives Bevy, Zig's `comptime`, and Rust's nightly reflection MVP full treatment, and then shows where CGP's type-level mirror of reflection lands — grounded in a real worked example, [`cgp-serde`](https://github.com/contextgeneric/cgp-serde)'s reflection-driven serializer, set against facet and the Rust MVP. It is the introspection counterpart to the runtime-mechanism comparison in [dynamic dispatch](dynamic-dispatch.md), and it leans on [row polymorphism](row-polymorphism.md) for the structural-typing theory underneath. + +## The concept in depth + +Reflection spans a spectrum a reader should keep ordered, from fully runtime to fully compile-time, because CGP sits at one precise end of it. At the runtime end, a type carries metadata the program inspects while it runs — Java's `Class`, Go's `reflect.Type`, C#'s reflection with attributes, Python's pervasive `__dict__`/`getattr`, and Bevy's `bevy_reflect`. In the middle, a derive generates a static descriptor that runtime code walks without re-monomorphizing, the approach Rust's facet takes. At the compile-time end, the compiler evaluates the introspection during compilation and emits only the specialized result — Zig's `comptime`, D's `__traits` and CTFE, C++26's static reflection (P2996, [voted into C++26 in June 2025](https://isocpp.org/files/papers/P2996R13.html), with its `^^` reflection operator, `std::meta::info` values, and `[: :]` splicers), and now the Rust nightly effort. The three systems this document treats in full — Bevy at the runtime end, Zig's `comptime` as the compile-time model, and Rust's MVP as the effort bringing that model to Rust — span the whole spectrum, and CGP sits just past the compile-time end of it, where the structure is not even a compile-time *value* but a *type*. + +The uses are the same all along the spectrum, which is why the comparison to CGP is about mechanism rather than purpose. Runtime and compile-time reflection alike power serialization (Go's `encoding/json` on struct tags, Java's Jackson, serde's derive, facet), dependency injection and frameworks (Spring reflecting over annotations), object-relational mapping (Hibernate), and tooling (editors, debuggers, test discovery). CGP's extensible-data machinery targets the same jobs — [serialization](https://github.com/contextgeneric/cgp-serde), builders that assemble a struct from independent parts, visitors over an enum's variants — so the question this document answers is not *what* reflection is for but *how* each system makes a type's shape available, and what that choice costs and guarantees. + +## Runtime reflection: Bevy + +Bevy's [`bevy_reflect`](https://docs.rs/bevy_reflect/latest/bevy_reflect/) is the prominent Rust runtime-reflection system, and it is the runtime counterpart to everything CGP does statically — a full facility for inspecting, accessing, and mutating a value's structure while the program runs. It exists because a game engine must do things the type system cannot fix at compile time: load a scene from disk into components whose types are chosen by the file, drive an inspector UI over arbitrary user components, and serialize a heterogeneous world without naming every type. `bevy_reflect` gives Rust — a language with no built-in reflection — that capability as a library, built from a derive, a trait, a descriptor, and a runtime registry. + +### The `Reflect` trait and dynamic value access + +The `Reflect` trait is what makes a value dynamically inspectable, and it is layered on a weaker `PartialReflect`. A type opts in with `#[derive(Reflect)]`, after which a value can be handled as a `Box` and accessed *by string field name* — reading or writing `"health"` on a component whose concrete type the calling code never mentions — and, because `Reflect` is stronger than `PartialReflect`, *downcast back to its concrete type at runtime* ([Bevy reflection overview, Tainted Coders](https://taintedcoders.com/bevy/reflection); [`Reflect` trait docs](https://docs.rs/bevy/latest/bevy/reflect/trait.Reflect.html)). This is the defining runtime move: the type is erased into `dyn Reflect`, and its structure is recovered by asking the value at run time rather than by knowing it at compile time. Field access by name returns an `Option`, so a wrong name is a runtime `None`, not a compile error. + +### `TypeInfo`: the static shape descriptor + +Backing the dynamic access is `TypeInfo`, a description of a type's shape that is available without an instance. The `#[derive(Reflect)]` macro generates a [`TypeInfo`](https://docs.rs/bevy/latest/bevy/reflect/enum.TypeInfo.html) — a `StructInfo`, `EnumInfo`, `TupleStructInfo`, and so on — enumerating the type's fields or variants with their names and registered types, "compile-time type information ... accessible at runtime without requiring an instance of the type." This is the descriptor a generic routine walks: a serializer iterates a `StructInfo`'s fields, a UI builds one widget per named field, and each is written once against `TypeInfo` rather than per concrete type. It is the runtime-reflection analogue of Zig's `@typeInfo` result and of CGP's `Fields` type — the same field-and-variant listing, made into a value read at run time. + +### The type registry and what it powers + +The piece that turns per-type descriptors into a working framework is the runtime `TypeRegistry`. The derive also generates a `GetTypeRegistration` impl, and registering a type stores its `TypeInfo` together with associated `TypeData` — extra per-type function tables, such as how to serialize it or reflect it as a component — in a [`TypeRegistry`](https://docs.rs/bevy/latest/bevy/reflect/struct.TypeRegistry.html), which Bevy keeps in the world as the `AppTypeRegistry` resource. With a type registered, Bevy can look it up by name or `TypeId` at run time, serialize an arbitrary registered component into a scene through a `DynamicSceneBuilder`, deserialize a scene back into live components, and drive an editor over types it was never specialized for. The registry is exactly what a compile-time system does not need and cannot have: a runtime table mapping types to their structure and behavior, consulted while the program runs. + +### The cost and the safety surface + +The power of runtime reflection is paid for in cost and safety, and Bevy's shape shows both plainly. Every reflective access goes through dynamic dispatch on `dyn Reflect`, a downcast, or a registry lookup keyed by type, none of which inline and each of which adds runtime work — the same overhead that makes reflection-based serialization a known hot-path cost elsewhere. And because access is by string name against a value whose type is erased, a mismatch — a renamed field, a type never registered, a wrong downcast — surfaces at run time as a `None`, an error, or a panic, rather than as a compile error. This is the runtime end of the spectrum in full: maximal flexibility, including over types discovered at run time, bought with runtime cost and runtime failure modes. It is what CGP trades entirely away. + +## Compile-time metaprogramming: Zig's `comptime` + +Zig's `comptime` is the reference design for compile-time reflection, and it is the closest functional analogue of what CGP achieves: the introspection runs during compilation and leaves zero trace in the program. Where Bevy inspects a value at run time, Zig inspects a *type* at compile time and emits only the specialized result, so a generic-over-structure routine compiles to exactly the code a hand-written one would. `comptime` is not a reflection API bolted onto the language but a general capability — running ordinary Zig code at compile time — of which type introspection is one use, which is precisely the generality the Rust effort admires and cannot yet match. + +### `comptime` values and parameters + +The foundation is that Zig can evaluate code and pass values — including types, which are first-class `comptime` values — at compile time. A parameter marked `comptime` must be known at compile time, and because a *type* is an ordinary `comptime` value of type `type`, a function can take a type as a parameter and compute with it during compilation ([Comptime, zig.guide](https://zig.guide/language-basics/comptime/)). Generic functions in Zig are just functions with `comptime` type parameters; there is no separate generics feature. This is the substrate reflection runs on: since types are values the compiler can inspect and manipulate, introspecting one is just calling a builtin on a `comptime` value. + +### `@typeInfo`: a type's structure as comptime data + +The builtin `@typeInfo(T)` decomposes a type into structured `comptime` data describing its shape. It returns a `std.builtin.Type` — a tagged union whose active field tells whether `T` is a `.Struct`, `.Enum`, `.Union`, `.Pointer`, `.Int`, and so on, each carrying the relevant details: a `.Struct` carries a `fields` array, each field a name, a type, and a default ([Zig type introspection, DeepWiki](https://deepwiki.com/ziglang/zig/4.3-type-introspection-and-metaprogramming); [*Zig Reflection: The @typeInfo Guide*](https://topictrick.com/blog/zig-type-reflection-typeinfo)). This is Zig's `TypeInfo`, but it is a *compile-time* value: it exists only during compilation, and code that reads it is evaluated away. It is the direct ancestor of Rust's nightly `Type`/`TypeKind` and the compile-time twin of Bevy's runtime `TypeInfo`. + +### `inline for` and `@field`: iterating the shape + +Reflection becomes useful through `inline for`, which unrolls a loop over a `comptime` collection, and `@field`, which accesses a field by a `comptime`-known name. The canonical pattern reads a type's fields with `@typeInfo` or `std.meta.fields`, iterates them with an unrolled `inline for`, and reaches each field's value with `@field`: + +```zig +fn jsonStringify(value: anytype, writer: anytype) !void { + inline for (std.meta.fields(@TypeOf(value))) |field| { + try writer.print("\"{s}\": ", .{field.name}); + try jsonStringify(@field(value, field.name), writer); + } +} +``` + +This writes one serializer that works over any struct, and because `inline for` unrolls at compile time and `@field` resolves statically, it compiles to exactly the code a hand-written serializer would — "the expressiveness of runtime reflection with the performance of hand-written code" ([*Compile-Time Reflection with @typeInfo*](https://hive.blog/hive-196387/@scipio/learn-zig-series-32-compile-time-reflection-with-typeinfo)). The `field.name` is the reflected field name recovered as a compile-time string, the exact counterpart of what every reflection system stores and of CGP's `Tag::VALUE`. + +### `@Type`: reflection in reverse + +Zig's introspection runs in both directions, which is a capability most reflection facilities lack: `@Type` *constructs* a type from a `std.builtin.Type` value. Where `@typeInfo` reads a type into data, `@Type` writes data back into a type, so a program can compute a new struct's field list at compile time and materialize it as a real type ([Zig type introspection, DeepWiki](https://deepwiki.com/ziglang/zig/4.3-type-introspection-and-metaprogramming)). This is genuine compile-time type synthesis — the analogue of C++26's splicers — and it is something CGP cannot do: CGP can build type-level *lists* and partial-record companions, but it cannot synthesize a brand-new nominal type from computed structure. + +### Zero cost, checked at instantiation + +The defining trade of `comptime` is that it is zero-cost but checked late. Because all of it runs at compile time, a `comptime` routine leaves no runtime metadata, no dispatch, and no cost — the payoff CGP also claims. But `comptime`, like C++ templates, is checked at *instantiation*: a generic `comptime` routine is only fully type-checked when applied to a concrete type, so an error inside it surfaces at the use site, per instantiation, in the template-error style Zig shares with C++. There is no separate declaration-site check that a `comptime` function is well-formed for all valid inputs. This is the one axis on which CGP diverges sharply from the model it otherwise mirrors, and the divergence is the subject of a later section. + +## Compile-time reflection comes to Rust + +Rust has historically had no reflection at all, and the effort to add compile-time reflection is now concrete enough to study directly from its source and tracking issues — a development that matters to CGP because its stated goal is to supply, from the compiler, exactly the structural information CGP's derives generate by hand. The picture has three parts: the userspace crates that filled the gap, the nightly compiler MVP and its current status, and the design questions still open before it can stabilize. + +### Why Rust had none, and the userspace answers + +Rust filled the absence of reflection with `#[derive]` proc macros that generate specialized code per type, and the cost of that choice is what motivates the reflection effort. The prominent userspace attempt, the [**facet**](https://fasterthanli.me/articles/introducing-facet-reflection-for-rust) crate, makes the argument sharply: serde's derives generate *monomorphized code* for each type, so `serde_json::to_string_pretty` is instantiated anew for every type — dozens of specialized copies — bloating compile times and binaries, with `syn` "often in the critical path of builds." Facet's `#[derive(Facet)]` instead generates *data*, not code: a compile-time `const SHAPE: &Shape` descriptor holding each field's name, offset, type shape, and function pointers, which generic *runtime* reflection code then walks once instead of being re-monomorphized per type. Facet is therefore runtime reflection driven by compile-time-generated const data — a point between Bevy's fully-runtime registry and Zig's fully-compile-time introspection. The narrower [`dtolnay/reflect`](https://github.com/dtolnay/reflect) is a proof of concept in a different direction: a compile-time reflection API for writing proc macros. + +### The nightly MVP: `core::mem::type_info` + +The nightly compiler effort is the more consequential development, and its status can be read directly from the source and the tracking issue. The [tracking issue #146922](https://github.com/rust-lang/rust/issues/146922) (opened September 2025, active into 2026) records the feature gate `#![feature(type_info)]` and consolidates an earlier [reflection tracking issue #142577](https://github.com/rust-lang/rust/issues/142577) that Oli Scherer (`oli-obk`) opened in June 2025 and that was closed as a duplicate. The API lives in [`library/core/src/mem/type_info.rs`](https://github.com/rust-lang/rust/blob/master/library/core/src/mem/type_info.rs) and is reached through two `const fn`s — `TypeId::info(self) -> Type` and `Type::of::() -> Type` — that return a `Type` whose single field is a `kind: TypeKind`: + +```rust +#![feature(type_info)] +use core::mem::{Type, TypeKind}; + +const KIND: TypeKind = Type::of::<(u32, bool)>().kind; // TypeKind::Tuple(..) +``` + +The MVP landed [tuples-first (PR #146923)](https://github.com/rust-lang/rust/pull/146923), but a rapid series of merged PRs has grown the surface far past that: `TypeKind` now has variants `Tuple`, `Array`, `Slice`, `DynTrait`, `Struct`, `Enum`, `Union`, `Bool`, `Char`, `Int`, `Float`, `Str`, `Reference`, `Pointer`, `FnPtr`, and `Other`, added by dedicated PRs for [ADTs — structs and enums (#151142)](https://github.com/rust-lang/rust/pull/151142), [trait objects (#151239)](https://github.com/rust-lang/rust/pull/151239), and [function pointers (#152173)](https://github.com/rust-lang/rust/pull/152173), among others. Crucially for the CGP comparison, struct and enum reflection is now real: a `Struct` exposes `fields: &'static [Field]` and its `generics`, an `Enum` exposes `variants: &'static [Variant]`, and each `Field` carries exactly the data a reflection consumer needs — `name: &'static str`, `ty: TypeId`, and `offset: usize`: + +```rust +// from library/core/src/mem/type_info.rs +pub struct Struct { + pub generics: &'static [Generic], + pub fields: &'static [Field], + pub non_exhaustive: bool, +} +pub struct Field { + pub name: &'static str, + pub ty: TypeId, + pub offset: usize, +} +``` + +Beyond the shape kinds, the surface already includes `TypeId::size()` and `TypeId::variants()` convenience queries, generics reflected as a `Generic` enum (`Lifetime`, `Type`, `Const`), and trait reflection through [`TypeId::trait_info_of` (PR #152003)](https://github.com/rust-lang/rust/pull/152003) returning a `TraitImpl` whose `get_vtable()` yields the `DynMetadata`. The current state, then, is not "tuples or bust" as the earliest write-ups described but a broad reflection surface covering nearly every type kind — with the remaining work being stabilization, not more kinds. + +### Compile-time-only: the `#[rustc_comptime]` restriction + +The single most important design decision is that this reflection is callable *only at compile time*, and the source enforces it explicitly. `TypeId::info` is documented "It can only be called at compile time," and the query methods carry a `#[rustc_comptime]` attribute — a compiler marker that pins them to compile-time evaluation. The reason is stated in the [project goal](https://rust-lang.github.io/rust-project-goals/2026/reflection-and-comptime.html): exposing type metadata to runtime code would force a global type table into every binary, the very cost that makes runtime reflection expensive. By restricting reflection to `const` contexts, Rust keeps the zero-cost property — the reflection is evaluated during compilation and nothing about it survives into the running program, exactly as in Zig `comptime`. A visible tension the source flags is lifetimes: `Type::of` can yield `TypeId`s "not necessarily derived from types that outlive `'static`," which "will be able to break invariants that other `TypeId` consuming crates may have assumed," an interaction with the non-`'static`-`TypeId` question ([#41875](https://github.com/rust-lang/rust/issues/41875)) still under discussion. + +### How Zig's `comptime` inspired it — and why Rust chose const-fn reflection + +The relationship to Zig's `comptime` is one of inspiration without adoption, and the project goal is explicit about it. Zig demonstrated that first-class compile-time type introspection combined with compile-time code execution can subsume both runtime reflection and macro-based codegen at zero cost, which is the capability the Rust effort wants — the goal is even named "reflection *and comptime*," and the `#[rustc_comptime]` marker borrows the word. But the goal **deliberately declines full Zig-style `comptime` for now**, "because the compiler is not set up in a way to permit proc macros from accessing type information from the current crate," treating general compile-time execution as future work that would need "more than 5 years" of compiler restructuring. The pragmatic path is a `const fn` that introspects types — `comptime`'s ambition narrowed to what Rust's architecture can deliver near-term: reflection first as a const API, general `comptime` much later, with Zig as the acknowledged model rather than the immediate target. + +### The road to stabilization: open questions and the RFC + +Despite the broad implemented surface, the feature is at the very start of the stabilization process, and the tracking issue is candid about what remains. Its checklist has every box unchecked — implement the goal, discuss general reflection with the lang team, discuss the non-`'static` question, write an RFC, update documentation, and only then a stabilization PR — and its unresolved questions cut to the foundations: whether to use many fine-grained intrinsics or one big intrinsic returning an enum, what semver guarantees reflection can retain, whether reflection should exist at all given that it "allows causing monomorphization-time errors ... for much more fine-grained details of a generic parameter `T`," whether anything lifetime-aware is possible, and whether a non-const-eval scheme (const generics, or even proc macros) would be better. The effort is driven by Oli Scherer as compiler champion with Scott McMurray (lang) and Josh Triplett (libs-api), and the plan spans 2026–2028: expand and refine the surface, validate it by giving `facet`, `bevy_reflect`, and `reflect` a nightly feature that "obsoletes having derives and makes the derives no-ops," and only then seek lang and libs-api buy-in and write the RFC. The ambition is that "crates like `bevy` will be able to 'just work' with arbitrary types instead of requiring authors to `#[derive(Component)]`." + +## How CGP expresses it + +CGP reaches compile-time reflection's payoff by a different route: it encodes a type's structure as *types* and processes them with *trait resolution*, so the "reflection" is done by the type system rather than by inspecting a descriptor. A struct's shape becomes a type-level list through a [derive](../reference/derives/derive_has_fields.md), generic code recurses over that list through trait impls the way Zig's `inline for` iterates `@typeInfo`, and the whole computation is checked at the definition site and erased before runtime. The [`cgp-serde`](https://github.com/contextgeneric/cgp-serde) crate makes the parallel concrete, and set against facet and the Rust MVP it shows exactly where the type-level encoding differs from value reflection. + +### A type's shape becomes a type, not a descriptor + +The foundational move is that `#[derive(HasFields)]` lifts a struct's structure into the type system as a type-level list, which is CGP's form of the `TypeInfo`/`Shape`/`std.builtin.Type`/`Type` descriptor the reflection systems build. Deriving it gives a struct a `Fields` associated type that is a [`Product!`](../reference/macros/product.md) of [`Field`](../reference/types/field.md) entries, and an enum a [`Sum!`](../reference/macros/sum.md) of them: + +```rust +#[derive(HasFields)] +pub struct Config { + pub host: String, + pub port: u16, +} + +// generated: +// impl HasFields for Config { +// type Fields = Product![ +// Field, +// Field, +// ]; +// } +``` + +This is the same information a reflection descriptor carries — each field's name and type — but it is a *type*, not a value. The Rust MVP's `Field` is a `struct` value with `name: &'static str` and `ty: TypeId`; CGP's `Field` is a *type* whose `Tag` is a type-level [`Symbol!`](../reference/macros/symbol.md) string and whose `Value` is the field's actual type as a type parameter. That difference — the field type carried as a *type* rather than an opaque `TypeId` value — is what lets CGP dispatch on it, and it is the crux of the comparison below. + +### `cgp-serde`: reflection-driven serialization in the trait system + +The `cgp-serde` crate's [`SerializeFields`](https://github.com/contextgeneric/cgp-serde/blob/main/crates/cgp-serde/src/providers/fields.rs) provider is a complete, working example of compile-time reflection expressed entirely through traits — one generic serializer that works over any `HasFields` type, the CGP counterpart of the Zig `jsonStringify` above. It serializes any value whose shape is known to the type system as a map, delegating to a trait that recurses over the field list: + +```rust +#[cgp_impl(new SerializeFields)] +impl ValueSerializer +where + Value: HasFields, + Value::Fields: FieldsSerializer, +{ + fn serialize(&self, value: &Value, serializer: S) -> Result + where + S: serde::Serializer, + { + let s = serializer.serialize_map(None)?; + Value::Fields::serialize_fields(self, value, s) + } +} +``` + +The `FieldsSerializer` trait is the `inline for` of CGP — a recursion over the [`Cons`/`Nil`](../reference/types/cons.md) product spine, with a recursive-step impl for a non-empty list and a base-case impl for the empty one: + +```rust +impl FieldsSerializer + for Cons, Rest> +where + Tag: StaticString, // the field name, as a const &'static str + Value: HasField, // read this field from the value + Context: CanSerializeValue, // serialize the field through the context's wiring + Rest: FieldsSerializer, // recurse on the remaining fields +{ + fn serialize_fields(context: &Context, value: &Value, mut serializer: S) + -> Result + { + let field_value = value.get_field(PhantomData); + serializer.serialize_entry(Tag::VALUE, &SerializeWithContext { context, value: field_value })?; + Rest::serialize_fields(context, value, serializer) + } +} + +impl FieldsSerializer for Nil { /* serializer.end() */ } +``` + +Every reflection concept has a precise counterpart here. The recursion over `Cons`/`Nil` is the loop over the field list; `Tag::VALUE` — the field name recovered as a compile-time `&'static str` through the [`StaticString`](../reference/traits/static_format.md) trait — is exactly the `field.name` a reflection system reads, produced with zero runtime metadata; `value.get_field(PhantomData)` is the by-name field access, resolved statically through [`HasField`](../reference/traits/has_field.md) rather than by a runtime lookup or a `@field` builtin; and `Context: CanSerializeValue` is the *recursive* step — serializing each field's value by dispatching on the field's *type*, which routes back through the context's own serialization wiring. The deserialization dual, [`DeserializeRecordFields`](https://github.com/contextgeneric/cgp-serde/blob/main/crates/cgp-serde/src/providers/record.rs), is even more visibly reflective: it reads a field name as a runtime `String` from the input map and compares it against each field's compile-time `Tag::VALUE`, routing the value into a `SetOptional` builder — reflection reading names in both directions. + +### `cgp-serde` versus facet versus the reflection MVP + +The three approaches solve the same problem — write serialization once, not per type — and comparing them precisely shows what the type-level encoding buys and costs. The axis that organizes them is *what a type's shape is turned into, and how the generic code consumes it*: + +- **serde's derive** generates *code*: a bespoke `Serialize` impl per struct. The logic is duplicated — monomorphized — for every type through generated code, which is the compile-time and binary bloat facet objects to. +- **facet** generates *data*: a `const SHAPE: &Shape` value per type, walked by a shared *runtime* reflection routine. It avoids re-monomorphizing the serializer per type, shrinking the binary, but pays runtime reflection cost to walk the shape and dispatch through the shape's function pointers. +- **the Rust MVP** has the *compiler* provide the data: `Type::of::()` yields a `Struct` with `fields: &'static [Field]`, needing no derive at all, consumed by `const`-evaluated code at compile time. +- **`cgp-serde`** generates *type-level data*: a `Product!` of `Field` via the `HasFields` derive, walked by a shared *compile-time* routine — `FieldsSerializer` trait recursion — that monomorphizes and inlines to direct code. + +Two differences make CGP's position distinct rather than merely another point. First, the field's *type* is preserved as a *type*, not erased to a value. Facet's `Shape` and the MVP's `Field { ty: TypeId, .. }` carry the field's type as an opaque descriptor — a `Shape` pointer or a `TypeId` — from which you cannot directly instantiate a generic function, so recursion into a field's own type is mediated by stored function pointers (facet) or is not yet expressible from the raw reflection (the MVP). CGP's `Field` carries `FieldValue` as a real type parameter, so `SerializeFields` can write `Context: CanSerializeValue` and recurse into a fully-typed, statically-checked serializer for the field's type — the recursion the MVP's `TypeId` cannot yet drive. Second, `cgp-serde` dispatches each field through the *context's* `CanSerializeValue` wiring, so the same type serializes differently under different application contexts — the [per-context choice](../concepts/coherence.md) that facet, serde, and the MVP, each with one fixed interpretation per type, do not have. + +The honest costs are equally clear. Unlike facet, `cgp-serde` does *not* solve the monomorphization-bloat problem — its `FieldsSerializer` recursion instantiates per field-list, so it produces specialized code per type just as serde's output does; what it saves is the *authoring* duplication (the logic is written once) and it gains *configurability* (the wiring), not binary size. And unlike the Rust MVP, `cgp-serde` requires the `#[derive(HasFields)]` opt-in: it cannot serialize a foreign type that did not derive the machinery, exactly the restriction the MVP is designed to remove for value-reflection consumers. + +### Checked when the code is written, not when it is instantiated + +The property that separates CGP from `comptime`, from C++ templates, and from the const-fn reflection MVP is *when* generic-over-structure code is checked, and CGP checks it early. Zig's `comptime` and the MVP's `const fn` reflection are checked at *instantiation*: a `comptime` routine or a reflection-driven `const fn` is fully checked only when applied to a concrete type, so an error surfaces at the use site, per instantiation. CGP's generic code is checked at its *definition*: the `where` clause on the `FieldsSerializer` impl — `Tag: StaticString`, `Value: HasField`, `Context: CanSerializeValue` — is verified once, against the bounds, and [`check_components!`](../reference/macros/check_components.md) verifies a context supplies everything its wiring transitively needs. This is the modular type-checking that [type classes](type-classes.md) give and templates do not, and CGP inherits it because its reflection is expressed as trait bounds the compiler checks up front rather than as code re-checked at each instantiation. + +### Reflection that also selects behavior and configures types + +CGP applies the same type-level-shape idea beyond inspecting data, to two things a structural reflection facility does not directly address: choosing implementations and fixing abstract types. Reflection systems inspect a type's fields and values; some frameworks then use that to select behavior — Spring wires beans by reflecting over annotations, Bevy associates `TypeData` with registered types — so behavior selection by reflection is real, but it is a runtime, registry-mediated act layered on top of the introspection. CGP folds behavior selection into the same compile-time type-level table that carries its structural information: a [component](../reference/macros/cgp_component.md) is keyed by a type-level marker, a context's [`delegate_components!`](../reference/macros/delegate_components.md) table maps markers to providers, and the [`#[cgp_type]`](../reference/macros/cgp_type.md) machinery lets a context fix an abstract *type* — its `Error`, its `Scalar` — through that same table with [`UseType`](../reference/providers/use_type.md). The `cgp-serde` serializer shows this in action: `Context: CanSerializeValue` resolves each field's serializer through the context, so the reflection that walks the shape and the wiring that interprets each field are one mechanism. + +### What CGP cannot do + +The runtime and open-introspection powers of reflection have no CGP analogue, and the boundary is worth stating plainly. Because CGP's structural information is a type computed at compile time, it cannot inspect a value whose type is only known at runtime, downcast a `dyn` value, serialize a type discovered from a plugin loaded at startup, or drive an editor over types registered in a runtime table — all of which are exactly what `bevy_reflect` exists to do, and for which Bevy's runtime reflection, not CGP, is the right tool. CGP also cannot *construct* an arbitrary new nominal type the way Zig's `@Type` or C++26's splicers can; its type-level `Product!`/`Sum!` lists and partial-record companions are a constrained, closed form of type construction, not open type synthesis. And, decisively, CGP's structural machinery is **opt-in per type**: it works only for types that `#[derive(HasFields)]` or [`#[derive(CgpData)]`](../reference/derives/derive_cgp_data.md), so it cannot introspect a foreign type that did not derive it — the same limitation facet and `bevy_reflect` have today, and precisely the restriction the nightly reflection MVP is being built to lift, though for value consumers rather than type-level ones. + +## What users like and dislike + +Reflection is one of the most relied-upon capabilities in mainstream software, and the reasons are consistent. It lets a framework be written once and work over every user type — serialization, dependency injection, ORMs, editors, test frameworks, and configuration all lean on it — which is why Java, C#, Go, and Python ship it and why ecosystems form around it. Runtime reflection additionally works on *arbitrary* types, including ones the framework author never saw, with no cooperation from the type beyond being reflectable, and compile-time reflection promises the same generality with none of the runtime cost — Zig's users prize getting "the expressiveness of runtime reflection with the performance of hand-written code," and the facet and Rust-reflection efforts are motivated by cutting the compile-time and binary bloat that per-type derive codegen imposes ([fasterthanli.me](https://fasterthanli.me/articles/introducing-facet-reflection-for-rust)). + +The complaints split cleanly by when the reflection runs. Runtime reflection is criticized on three counts: *performance* — Go's reflection-based `encoding/json` is "fundamentally slow" because it re-inspects types on every call, and Java reflection is a well-known hot-path cost ([*The Hidden Cost of Reflection in Go*](https://dev.to/devflex-pro/the-hidden-cost-of-reflection-in-go-why-your-code-is-slower-than-you-think-41ee)); *type safety* — it "bypasses compile-time type safety" and produces "stringly typed" access where a field named by a `json:"…"` tag or a `getField("name")` call fails at runtime, "quite like a dynamically typed language" ([Go reflection guide](https://medium.com/@mojimich2015/golang-reflection-the-guide-to-runtime-type-inspection-manipulation-and-best-practices-303087684576)); and *tooling and safety* — renaming a field silently breaks reflective access, dead-code elimination must be defeated to keep metadata, and reflection can bypass access control. Java's *type erasure* adds a further limit: reflection cannot distinguish `List` from `List` at runtime because the parameter is gone ([*Linguistic Reflection in Java*](https://arxiv.org/pdf/cs/9810027)). Compile-time reflection answers the performance and much of the safety objection — the checks run before the program does — but trades them for its own costs: instantiation-time error messages in the template/`comptime` style, added compile-time work, the conceptual weight of metaprogramming, and, as the Rust tracking issue frankly asks, the risk of enabling "monomorphization-time errors ... for much more fine-grained details of a generic parameter" that make generic code fail deep inside instantiation rather than at its interface. + +## How CGP compares + +CGP and reflection make opposite choices on the two axes that organize the whole spectrum — *when the structure is available* and *what form it takes* — and the comparison is cleanest as that pair. On *timing*, runtime reflection makes structure available while the program runs, paying dispatch and inspection cost on every use and deferring errors to runtime; compile-time reflection and CGP both resolve it during compilation and pay nothing at runtime. On *form*, every reflection system — runtime or compile-time — makes the structure into *data* that code inspects: a `TypeInfo`, a `Shape`, a `std.builtin.Type`, a `Type`; CGP makes it into a *type* that trait resolution dispatches on. That second difference is what buys CGP its distinctive properties: because the structure is a type and the field types are carried as types, generic code is trait impls checked modularly at their definition rather than per instantiation, it can recurse into a field's own type with full static checking, and the same type-level encoding extends seamlessly from inspecting data to selecting providers and configuring abstract types. What CGP gives up for this is generality and runtime reach — it introspects only types that opted in by deriving its machinery, and only at compile time. + +The honest positioning names where each wins, and here it comes with an unusual twist: CGP and Rust's emerging compile-time reflection are more complementary than competing. When a program must inspect types at runtime — deserialize into a type chosen from a config file, serialize a heterogeneous registry of components, build an editor or a debugger over live values, or reflect over a foreign type that cannot be made to derive anything — runtime reflection is the right and only tool of the two, and `bevy_reflect` or facet, not CGP, is what to reach for. When a program wants generic-over-structure code that costs nothing at runtime, is checked when written, recurses into field types with full type information, and drives behavior and type selection as well as data walking, CGP delivers that on stable Rust today, as `cgp-serde` shows. The two lines could converge, but less directly for CGP than for facet or Bevy: the nightly MVP produces reflection as *const values* (`Type`, `Field`, `TypeId`), which value-reflection libraries consume natively and which the project goal aims to let them use with no derive — whereas CGP consumes reflection as *types* (`Product!`, `Field`), a different projection the const-value MVP does not yet supply. For CGP to shed its `#[derive(HasFields)]` the way the goal envisions for `#[derive(Reflect)]`, the compiler would need to expose a type's shape *as types* — a type-level counterpart to the value-level MVP — which is not part of the current scheme. Until then CGP's derive stands in for the compiler-provided reflection Rust is still building, and the reflection MVP is best read not as a rival to CGP but as the same underlying idea, reflected into values where CGP reflects it into types. + +## Presenting CGP to someone who knows this + +A reader who thinks in `TypeInfo`, `@typeInfo`, or `core::mem::Type` holds most of CGP's structural machinery already, and the way in is to map the vocabulary and then mark the one shift. A `#[derive(HasFields)]` type's `Fields` is a **compile-time type descriptor**, exactly like a `Shape`, a `std.builtin.Type`, or the MVP's `Struct { fields, .. }` — except it is a *type*, not a value; a generic impl recursing over the [`Cons`/`Nil`](../reference/types/cons.md) spine, as `cgp-serde`'s `FieldsSerializer` does, is an **`inline for` over the fields**, resolved by the trait system instead of a `comptime` loop or a const-fn walk; `Tag::VALUE` is the reflected **field name**; and [`check_components!`](../reference/macros/check_components.md) is the guarantee that the generic code type-checks for this concrete type, discharged when the code is written rather than at instantiation. Framed this way, CGP is not a foreign paradigm to this reader but *compile-time reflection encoded in the type system* — the Zig `comptime` idea they know, expressed as trait resolution over type-level shapes, with modular checking they may wish `comptime` and templates had. + +The expectation to correct at once is that CGP offers a reflection *API* — an object to inspect, a descriptor to query, a value to walk. It does not: there is no `TypeInfo` to hold, no `Field` slice to iterate imperatively, no `Type::of::()` to call. The structure is a type that trait impls dispatch on, so "reflecting" over it means writing a recursive impl, not calling `fields()`. For the Zig or C++26 reader, the sharp framing is that CGP's `Product!`/`Sum!` are the `@typeInfo` result reified as types and its trait recursion is the `inline for`, but CGP cannot `@Type`-construct arbitrary new types and cannot introspect a type that did not derive the shape — a real limit against a true reflection facility. For the Rust reflection reader specifically, the precise point is that CGP carries a field's *type as a type* where the MVP's `Field` carries it as an opaque `TypeId`, which is why CGP can recurse into a typed, checked serializer for each field and the raw MVP cannot yet — the two encode the same shape but project it into different domains, values versus types. For the Bevy or Java reader, the pitch is the pair of things runtime reflection makes them pay for and CGP does not: *no runtime cost*, because the introspection is resolved away instead of walked on every call, and *no stringly-typed runtime failure*, because a field or capability the context lacks is a compile error at the wiring site rather than a `None` or an exception in production. + +The analogy to avoid is calling CGP "a reflection system." It has no runtime type information, no descriptor to inspect, and no ability to reflect over a type that did not opt in — a reader sold on that framing will look for an introspection API and find trait bounds instead. Say precisely what CGP is: compile-time structural reflection encoded as types and resolved by the trait system, checked at the definition site, erased before runtime, carrying field types as types so it can recurse into them, and extended from inspecting a type's data to selecting its behavior and configuring its abstract types. A reader who has paid the runtime tax of reflection, or fought a template's instantiation-time errors, will hear that as a considered trade rather than a missing feature. + +## Sources + +The account of the related work draws on the official documentation and primary write-ups of each reflection system, the Rust project's own tracking issues, pull requests, and library source for its compile-time-reflection effort, and cited community writing for sentiment; the CGP snippets are drawn from the [`cgp-serde`](https://github.com/contextgeneric/cgp-serde) crate and the knowledge base's [extensible records](../concepts/extensible-records.md) and [extensible variants](../concepts/extensible-variants.md) material, verified against current macro behavior. + +- [Bevy `bevy_reflect` documentation](https://docs.rs/bevy_reflect/latest/bevy_reflect/), [`Reflect` trait](https://docs.rs/bevy/latest/bevy/reflect/trait.Reflect.html), [`TypeInfo`](https://docs.rs/bevy/latest/bevy/reflect/enum.TypeInfo.html), [`TypeRegistry`](https://docs.rs/bevy/latest/bevy/reflect/struct.TypeRegistry.html), and [Bevy Reflection (Tainted Coders)](https://taintedcoders.com/bevy/reflection) — the `Reflect`/`PartialReflect` traits, `TypeInfo`, the runtime `TypeRegistry`/`TypeRegistration` and `AppTypeRegistry`, `#[derive(Reflect)]`, downcasting, `TypeData`, and scene serialization that make Bevy the Rust runtime-reflection exemplar. +- [Zig type introspection and metaprogramming (DeepWiki)](https://deepwiki.com/ziglang/zig/4.3-type-introspection-and-metaprogramming), [Comptime (zig.guide)](https://zig.guide/language-basics/comptime/), [*Zig Reflection: The @typeInfo Guide*](https://topictrick.com/blog/zig-type-reflection-typeinfo), and [*Compile-Time Reflection with @typeInfo*](https://hive.blog/hive-196387/@scipio/learn-zig-series-32-compile-time-reflection-with-typeinfo) — `comptime` values and parameters, `@typeInfo`/`@Type`, `std.meta.fields`, `inline for`, `@field`, and the zero-runtime-cost, instantiation-checked model that inspired the other systems languages. +- [Rust tracking issue #146922 (`type_info`)](https://github.com/rust-lang/rust/issues/146922), [tracking issue #142577 (reflection, closed as duplicate)](https://github.com/rust-lang/rust/issues/142577), the source at [`library/core/src/mem/type_info.rs`](https://github.com/rust-lang/rust/blob/master/library/core/src/mem/type_info.rs), and the implementation PRs [#146923 (MVP)](https://github.com/rust-lang/rust/pull/146923), [#151142 (ADTs)](https://github.com/rust-lang/rust/pull/151142), [#151239 (trait objects)](https://github.com/rust-lang/rust/pull/151239), [#152003 (`trait_info_of`)](https://github.com/rust-lang/rust/pull/152003), and [#152173 (`FnPtr`)](https://github.com/rust-lang/rust/pull/152173) — the exact API (`Type::of`, `TypeId::info`, `TypeKind`, `Struct`/`Field`/`Variant`), the `#[rustc_comptime]` compile-time-only restriction, the current implemented surface, and the open stabilization checklist and design questions. +- [Rust Project Goals — *Reflection and comptime* (2026)](https://rust-lang.github.io/rust-project-goals/2026/reflection-and-comptime.html) and [*Compile-Time Reflection Is Finally Here*](https://weeklyrust.substack.com/p/compile-time-reflection-is-finally) — the plan to make reflection-crate derives no-ops, the explicit rejection of full Zig-style `comptime` near-term, the 2026–2028 timeline, and the leadership (Oli Scherer, Scott McMurray, Josh Triplett). +- [fasterthanli.me, *Introducing facet: Reflection for Rust*](https://fasterthanli.me/articles/introducing-facet-reflection-for-rust) and [`dtolnay/reflect`](https://github.com/dtolnay/reflect) — facet's derive-generates-data approach (a `const Shape` walked by runtime reflection) and its motivation in serde's monomorphization cost, and a compile-time reflection API for proc-macro authors. +- [`cgp-serde` `SerializeFields`](https://github.com/contextgeneric/cgp-serde/blob/main/crates/cgp-serde/src/providers/fields.rs) and [`DeserializeRecordFields`](https://github.com/contextgeneric/cgp-serde/blob/main/crates/cgp-serde/src/providers/record.rs) — the worked CGP example: a generic serializer and deserializer that recurse over a type's `HasFields` shape through trait resolution, recovering field names via `StaticString` and dispatching each field through the context's `CanSerializeValue` wiring. +- [P2996R13, *Reflection for C++26*](https://isocpp.org/files/papers/P2996R13.html) and [*Reflection in C++26 (P2996)*](https://learnmoderncpp.com/2025/07/31/reflection-in-c26-p2996/) — the `^^` reflection operator, `std::meta::info`, `consteval` metafunctions, and splicers `[: :]`, voted into C++26 in June 2025, the C++ realization of compile-time reflection. +- [Go FAQ — reflection and interfaces](https://go.dev/doc/faq), [*The Hidden Cost of Reflection in Go*](https://dev.to/devflex-pro/the-hidden-cost-of-reflection-in-go-why-your-code-is-slower-than-you-think-41ee), [*Golang Reflection guide*](https://medium.com/@mojimich2015/golang-reflection-the-guide-to-runtime-type-inspection-manipulation-and-best-practices-303087684576), and [*Linguistic Reflection in Java*](https://arxiv.org/pdf/cs/9810027) — the `reflect` package and struct tags, the runtime performance and type-safety ("stringly typed") costs, and Java's type-erasure limitation, the mainstream runtime-reflection landscape CGP contrasts with. diff --git a/docs/related-work/row-polymorphism.md b/docs/related-work/row-polymorphism.md new file mode 100644 index 00000000..90b9393c --- /dev/null +++ b/docs/related-work/row-polymorphism.md @@ -0,0 +1,182 @@ +# Row polymorphism, structural typing, and extensible data types + +Row polymorphism, structural typing, and extensible data types are the family of type-system features that let code operate on data by the *shape* of its fields and variants — "any record that has a `name` field," "any enum that includes a `Circle` case" — rather than by a concrete type's declared name. CGP brings the same field-and-variant-driven extensibility to Rust's resolutely nominal structs and enums, not through a built-in row kind but by deriving a type-level view of a type's shape and matching field and variant names through the trait system at compile time. + +## Purpose + +Nominal type systems, Rust's included, force code to name the concrete type it works on, which couples every operation to a fixed shape. A Rust function that reads a `name` field must take a specific struct, a `match` must spell out one specific enum's variants, and two structs with identical fields but different names are incompatible types. This is safe and predictable, but it blocks a large class of useful code: a function that works on *any* record carrying the fields it needs, a handler that covers *one* variant of *any* enum that includes it, or a record assembled piecemeal from parts that never name the whole. The features surveyed here exist to lift that restriction — to let a type's structure, not its name, decide what operations apply. + +This is precisely the capability CGP's [extensible records](../concepts/extensible-records.md) and [extensible variants](../concepts/extensible-variants.md) add to Rust, so the comparison is not incidental but foundational: the knowledge base's own account of extensible records says the machinery "brings the row-polymorphism of languages like PureScript and the structural typing of records to Rust." A reader who already thinks in rows and structural shapes holds most of the intuition CGP's data machinery rests on. The work of this document is to make that correspondence exact — to show which CGP construct is the row variable, which is record concatenation, which is variant injection — and to be honest about the one place the analogy breaks: CGP has no row *inference*, so where a row-typed language discovers the shape for you, CGP asks you to derive it and wire it. + +## The concept in depth + +The three phrases in this document's title name overlapping but distinct ideas, and the comparison to CGP is clearest when they are kept apart. *Structural typing* is the broad principle that type compatibility is decided by structure rather than by name. *Row polymorphism* is a specific parametric mechanism — a quantified *row variable* standing for "the rest of the fields" — that achieves structural flexibility while preserving the extra fields' types. *Extensible data types* are the records and variants a row system operates on, and their common theoretical account is the subject of the research paper this document studies. The sections below build up from the general principle to the specific mechanism, work PureScript as the primary example of row polymorphism, distinguish the two ways rows are realized — as a built-in kind or as qualified-type predicates — cover the variant (sum) side, and close on the unifying theory, the third application to effects, and two cautionary tales. + +### Structural versus nominal typing + +Structural typing decides type compatibility by a type's members, not its name. A structural type system is "one in which type compatibility and equivalence are determined by the type's actual structure or definition and not by other characteristics such as its name or place of declaration"; a value of type `A` is usable where `B` is expected when `A` has all of `B`'s members with compatible types ([Wikipedia: Structural type system](https://en.wikipedia.org/wiki/Structural_type_system)). A *nominal* system is the opposite — compatibility rests on "explicit declarations and/or the name of the types," so two `struct`s with identical fields but different names are never interchangeable ([Wikipedia: Nominal type system](https://en.wikipedia.org/wiki/Nominal_type_system)). + +TypeScript is the mainstream face of structural typing. Its checker "focuses on the shape that values have," an approach it calls "duck typing or structural typing," and an object satisfies an interface merely by having the required members, with no declaration that it implements the interface ([TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-oop.html)). Crucially, structural typing is *not* the same as row polymorphism: TypeScript "doesn't have 'true' row type polymorphism in the sense of ML-family languages" and instead approximates it with generic constraints, mapped types, and intersection types ([Bornea, "Approximating Row Type Polymorphism in TypeScript"](https://medium.com/@gabriel-bornea/approximating-row-type-polymorphism-in-typescript-495ebd5a623d)). Go makes a narrower structural choice: a type satisfies an interface simply by having the required methods, "no keywords, declarations, or ceremony required" ([Go specification](https://go.dev/ref/spec#Interface_types)), though Go's structs are otherwise nominal. Scala 3 offers opt-in structural types through the `Selectable` trait and refinement syntax, but does not infer them — you write the structure down ([Scala 3 Reference](https://docs.scala-lang.org/scala3/reference/changed-features/structural-types.html)). Rust sits firmly at the nominal end: its structs and enums are nominal, it "notably lacks a built-in, structural type option," and row polymorphism "is notably absent from Rust's struct system" ([Andreas, "Structural vs Nominal Typing in Rust"](https://felixandreas.me/blog/nominal-vs-structural-types/)). That absence is the ground CGP works on. + +### Row polymorphism proper, in PureScript + +Row polymorphism is structural flexibility delivered by a *row variable* rather than a subtype relation. Where structural subtyping coerces a wider value to a narrower type, row polymorphism quantifies over "the rest of the fields," so a function accepts any record that has *at least* the fields it names while a type variable carries the others through unchanged ([Wikipedia: Row polymorphism](https://en.wikipedia.org/wiki/Row_polymorphism)). PureScript is the clearest working example, because rows are the foundation of its record system rather than a library bolted on. + +In PureScript a *row* and a *record* are different things. A row is "an unordered collection of named types, with duplicates," of kind `Row k` — not itself a value ([PureScript language reference](https://github.com/purescript/documentation/blob/master/language/Types.md)) — and a record is a row wrapped by the `Record` constructor, `Record :: Row Type -> Type`, for which the brace form `{ name :: String }` is sugar for `Record ( name :: String )` ([Pursuit: Prim](https://pursuit.purescript.org/builtins/docs/Prim)). An *open* row adds a tail variable after a pipe, which is where the polymorphism lives: + +```purescript +-- accepts any record that has at least firstName and lastName; +-- `r` captures whatever other fields the caller's record carries +fullName :: forall r. { firstName :: String, lastName :: String | r } -> String +fullName p = p.firstName <> " " <> p.lastName + +fullName { firstName: "Ada", lastName: "Lovelace" } -- ok +fullName { firstName: "Ada", lastName: "Lovelace", age: 36 } -- also ok; age flows through `r` +``` + +The record operations that make rows useful are governed by the `Prim.Row` type classes, which are relations over rows resolved during type checking. `Cons` "asserts that one row of types can be obtained from another by inserting a new label/type pair"; `Union` computes "the union of two rows of types (left-biased, including duplicates)"; `Nub` removes "duplicate labels from rows"; and `Lacks` "asserts that a label does not occur in a given row" ([Pursuit: Prim.Row](https://pursuit.purescript.org/builtins/docs/Prim.Row)). On top of these sit the everyday operations — field access `p.field`, functional update `p { field = newValue }`, and record merge — and libraries such as `purescript-record` and `purescript-simple-json`, the latter decoding JSON straight into a record type with no hand-written codec by reflecting over the row at the type level ([purescript-simple-json](https://github.com/justinwoo/purescript-simple-json)). + +PureScript's specific design — rows that permit *duplicate* labels with scoping — is Daan Leijen's, and the lineage is worth naming because it recurs across the field. Row variables originate with Mitchell Wand's "Complete Type Inference for Simple Objects" (LICS 1987), which introduced row variables but assumed labels were unique ([Wand 1987](https://www.ccs.neu.edu/home/wand/papers/wand-lics-87.pdf)); Didier Rémy made rows work cleanly inside ML with principal types (POPL 1989) ([Rémy 1989](https://dl.acm.org/doi/10.1145/75277.75284)); and Leijen's "Extensible records with scoped labels" (TFP 2005) dropped the uniqueness restriction, allowing duplicate labels and "effectively introducing a form of scoping over the labels" ([Leijen 2005](https://www.microsoft.com/en-us/research/publication/extensible-records-with-scoped-labels/)). PureScript adopts this directly: it "allows duplicate labels in rows, and the meaning of `Record r` is based on the first occurrence of each label," and its creator Phil Freeman cites Leijen's paper and prototyped the design in a repository named `purescript-scoped-labels` ([Pursuit: Prim](https://pursuit.purescript.org/builtins/docs/Prim); [Freeman, PureScript type-system talk](https://speakerdeck.com/paf31/an-overview-of-the-purescript-type-system)). + +### Rows as a kind versus rows as predicates + +Two designs realize row polymorphism, and the difference decides how naturally it ports to a language like Rust. PureScript, following Wand, Rémy, and Leijen, makes rows a *built-in kind* — `Row k` is a sort of type the compiler knows, with its own unification — so record and variant types are wrapped rows and the row variable is a first-class type-level entity. The alternative, due to Benedict Gaster and Mark Jones, encodes rows through *qualified types* instead: a function is polymorphic over an ordinary type variable, and the constraints that variable must satisfy — a *lacks* predicate `r\l` ("row `r` has no label `l`") and its dual *has* predicate — are discharged by the same constraint-resolution machinery that resolves type-class instances, with no new kind ([Gaster & Jones, *A Polymorphic Type System for Extensible Records and Variants*](http://web.cecs.pdx.edu/~mpj/pubs/polyrec.html)). Gaster and Jones designed their system precisely so it could be "understood and implemented as a natural extension" of Haskell and ML, because it reuses the qualified-types machinery already present for type classes. + +This second design is the one CGP most closely resembles, and naming it corrects the reflex to say "CGP has no rows." CGP has no row *kind* and no row *variable*, but it does have row *predicates*: a `HasField` bound is a has-predicate over a context's row, resolved by the trait solver exactly as Gaster and Jones resolve their constraints by qualified-type inference. Where PureScript threads a row variable, CGP threads a *context* type variable and constrains it — the predicate-based rows of Gaster–Jones, carried by Rust's traits rather than a bespoke row kind. A further refinement of the kind-based systems, Rémy's *presence polymorphism*, encodes not merely which labels are present but a per-label *flag* recording presence or absence, so a function can be polymorphic in whether a given field is there at all ([Rémy 1989](https://dl.acm.org/doi/10.1145/75277.75284); [Row polymorphism, HandWiki](https://handwiki.org/wiki/Row_polymorphism)); CGP reproduces exactly this with the per-field presence markers shown later. + +### Extensible variants: the sum-side dual + +Everything above concerns records — products of named fields — and it has an exact dual for enums, the *sums* of named variants. Where a row-polymorphic record function accepts "at least these fields," a row-polymorphic variant value promises "at most these cases," and the same row machinery types both. OCaml is the canonical industrial home of this dual through its *polymorphic variants*, which need no central type declaration and whose tags are written with a backtick. Their types carry bounds in either direction: an open type `` [> `On ] `` "accepts at least the specified tags but may be extended," while a closed type `` [< `On | `Off ] `` restricts to the listed ones ([OCaml manual](https://ocaml.org/manual/5.4/polyvariant.html)): + +```ocaml +(* no type declaration needed; the variant type is inferred and reusable *) +let to_int = function + | `On -> 1 + | `Off -> 0 + | `Number n -> n +(* inferred: [< `Number of int | `Off | `On ] -> int *) +``` + +Extensible variants and records are the classic answer to the **expression problem**, Philip Wadler's name for the challenge of adding both new cases to a datatype *and* new operations over it "without recompiling existing code, and while retaining static type safety" ([Wadler 1998](https://homepages.inf.ed.ac.uk/wadler/papers/expression/expression.txt)). Wadler's own framing is a table: "cases as rows and functions as columns" — functional languages fix the rows and add columns easily, object-oriented languages fix the columns and add rows easily, and extensible data types let you add both. That table is why row-typed records and variants matter beyond convenience, and it is the same problem CGP's [extensible variants](../concepts/extensible-variants.md) set out to solve. + +Haskell realizes the constraint-based route for the sum side directly in Wouter Swierstra's *Data types à la carte*, the canonical functional answer to the expression problem and the closest cousin to CGP's variant machinery. It represents an extensible datatype as a *coproduct of signature functors* — `f :+: g`, the type-constructor analogue of `Either` — together with a type-class constraint `f :<: g` ("`f` is a sub-signature of `g`") whose evidence supplies an injection `inj :: f a -> g a` and a partial projection `prj :: g a -> Maybe (f a)` ([Swierstra, *Data types à la carte*](https://www.cs.tufts.edu/~nr/cs257/archive/wouter-swierstra/DataTypesALaCarte.pdf)). New cases and new operations both slot in without editing existing code, and — the point for this comparison — the whole construction is built from *constraints resolved like type classes* rather than a built-in row kind: the Gaster–Jones route applied to sums, with `inj`/`prj` doing the injection and projection that a row system's containment predicate governs. + +### The unifying theory: "rows by any other name" + +Beneath the language-specific designs lies a general theory, and the research paper this document studies — J. Garrett Morris and James McKinna's "Abstracting extensible data types: or, rows by any other name" (POPL 2019) — is its most complete statement. The paper presents "a novel typed language for extensible data types, generalizing and abstracting existing systems of row types and row polymorphism," introducing **row theories**, "a monoidal generalization of row types, giving a general account of record concatenation and projection (dually, variant injection and branching)" ([Morris & McKinna 2019, abstract](https://researchportal.hw.ac.uk/en/publications/abstracting-extensible-data-types-or-rows-by-any-other-name/); [ACM DOI 10.1145/3290325](https://dl.acm.org/doi/10.1145/3290325)). The value of the abstraction is that Wand's, Rémy's, and Leijen's differing systems become *instances* of one framework rather than rival designs. + +The framework rests on two predicates over rows, and their shapes map so cleanly onto CGP that they are worth stating precisely. The **combination** predicate `ρ₁ ⊙ ρ₂ ~ ρ₃` says that rows `ρ₁` and `ρ₂` combine to yield `ρ₃`, and the **containment** predicate `ρ₁ ≲ ρ₂` says one row is included in another ([Morris & McKinna, via the follow-up "Generic Programming with Extensible Data Types"](https://arxiv.org/pdf/2307.08759)). Records and variants are then perfect duals over these predicates: a record is *built* by concatenation (combination) and *read* by projection (containment), while a variant is *built* by injection (containment) and *consumed* by branching (combination). The framework is parametric over the *row theory*, which fixes how rows combine — "simple rows" forbid overlap and combine commutatively, "scoped rows" (Leijen's) are non-commutative and "particularly well suited to capturing algebraic effects" — and it maintains strong metatheory throughout, including coherence and principal types. Its deepest idea for the CGP comparison is that "evidence for type qualifiers has computational content, determining the implementation of record and variant operations": the proof that a row contains a label, or that two rows combine, is not a static checkmark but a runtime value that *is* the projection or concatenation code. The Haskell `row-types` library (`Data.Row`) is a direct implementation of this paper ([Hackage: Data.Row.Records](https://hackage.haskell.org/package/row-types-1.0.1.2/docs/Data-Row-Records.html)). + +### A third application: effect rows + +Rows type more than data, and their most prominent third use — *effects* — matters here because it is where row polymorphism most visibly both succeeds and fails. Koka types a computation's side effects as a *row* of effect labels, so `` in a signature means "may throw and diverge," applying the exact row polymorphism this document describes to effects instead of fields ([Leijen, *Koka: Programming with Row-Polymorphic Effect Types*](https://arxiv.org/pdf/1406.2061)). This is why Morris and McKinna single out *scoped* rows as "particularly well suited to capturing algebraic effects": an effect row, like a scoped record row, is an unordered collection of labels where duplicates and ordering carry meaning. The [algebraic effects](algebraic-effects.md) comparison develops the effect side in full; the point for rows is that one mechanism serves records, variants, *and* effects — and that its record and effect uses have diverged sharply in practice, since effect rows are precisely the ones languages have retreated from, as the next section shows. + +CGP's counterpart to an effect row is not a separate row kind but the same predicate set it uses for data: a provider's [impl-side dependencies](../concepts/impl-side-dependencies.md) are the capabilities it may use, the way an effect row is the effects a function may perform, and [`check_components!`](../reference/macros/check_components.md) discharges them the way a handler discharges an effect. Where the row languages keep a distinct row kind for each of records, variants, and effects, CGP collapses all three onto trait predicates over a context — a point the [algebraic effects](algebraic-effects.md) document takes up from the effect side. + +### Two cautionary tales: PureScript effects and Gleam records + +Two languages that once had prominent row systems removed them, and the reasons are directly relevant to CGP's positioning. PureScript's original effect monad, `Eff`, tracked side effects in a *row* of effect types; version 0.12 replaced it with a monomorphic `Effect` that drops the effect row, because "getting the effect rows to line up was sometimes quite tricky, without providing a great deal of benefit," the rows "were not sufficiently useful in terms of actually guaranteeing what effects were run," and — the recurring theme — they "led to many problems with users not knowing how to solve type errors with unification of effect rows" ([PureScript-Resources, "Why did PureScript go from Eff to Effect?"](https://purescript-resources.readthedocs.io/en/latest/eff-to-effect.html)). PureScript kept row-typed *records*, but retreated from row-typed *effects*. + +Gleam went further and removed row-typed records outright. Early Gleam typed records as Erlang maps with genuine row polymorphism, but version 0.4 removed them, making the type system "simpler, less structural and more nominal in style" ([Gleam v0.4 release notes](https://gleam.run/news/gleam-v0.4-released/)). Current Gleam is nominal: its creator states "all values have to belong to a type" ([GitHub discussion #3223](https://github.com/gleam-lang/gleam/discussions/3223)), and what resembles row polymorphism today — the `.field` accessor across variants and the "variant inference" added in v1.6.0 — is flow-sensitive local narrowing that "does not persist across function boundaries," not row polymorphism at all ([welltypedwitch, "Nominal for storing, structural for manipulating"](https://welltypedwitch.bearblog.dev/nominal-for-storing-structural-for-manipulating/)). Both retreats were driven substantially by the cost of row-unification error messages — a cost any comparison to CGP must take seriously, because CGP's type-level machinery is subject to the same failure mode. + +## How CGP expresses it + +CGP has no row kind, no anonymous record type, and no row variable; it keeps Rust's nominal structs and enums and *derives* a type-level description of a type's shape, then matches field and variant names through the trait system. The result is the same field-and-variant-driven extensibility a row system provides, reached by the predicate-based route rather than the kind-based one: a type's shape is a type-level list, and the row predicates become trait constraints resolved by the trait solver — the Gaster–Jones qualified-types style [described above](#rows-as-a-kind-versus-rows-as-predicates), carried by Rust's traits. The two halves are deliberately dual, mirroring the record/variant duality of row theories. + +### A struct is a closed row; `HasField` is row containment + +The [`#[derive(HasFields)]`](../reference/traits/has_fields.md) derive gives a struct the type-level equivalent of a closed row — a [`Product!`](../reference/macros/product.md) of [`Field`](../reference/types/field.md) entries, each tagged by a [`Symbol!`](../reference/macros/symbol.md) type-level string, one per field: + +```rust +#[derive(HasField, HasFields)] +pub struct Person { + pub first_name: String, + pub last_name: String, +} + +// generated: +// type Fields = Product![ +// Field, +// Field, +// ]; +``` + +Where PureScript's `fullName` names its required fields with an open row `{ firstName :: String | r }`, a CGP provider names them as [impl-side dependencies](../concepts/impl-side-dependencies.md): a `Context: HasField` bound, usually written as an [`#[implicit]`](../reference/attributes/implicit.md) argument or a getter. That bound is exactly the containment predicate `ρ₁ ≲ ρ₂` from the row-theory framework — "this row contains this label of this type" — and any context whose derived row includes the field satisfies it, with the context's other fields playing the role of the tail variable `r`. The difference from PureScript is that the row variable is implicit and never named: CGP does not infer or carry a residual row, it simply resolves the containment bound against whatever concrete context is wired. + +### Building a record is row combination + +Assembling a struct field by field is CGP's record concatenation, the combination predicate `ρ₁ ⊙ ρ₂ ~ ρ₃`. The [builder family](../reference/traits/has_builder.md) walks a *partial record* from empty to complete, and [`CanBuildFrom`](../reference/traits/cast.md) absorbs the shared fields of one struct into another's builder in a single step — the direct analogue of PureScript's `Record.union` and the paper's record concatenation: + +```rust +let combined: FooBarBaz = FooBarBaz::builder() + .build_from(FooBar { foo: 1, bar: "bar".into() }) // splice one row in + .build_from(Baz { baz: true }) // splice another + .finalize_build(); // exists only when the row is complete +``` + +The underlying type-level operations are literally a row algebra: [`ConcatProduct`](../reference/traits/product_ops.md) splices two products, `AppendProduct` adds one field, and `MapFields` rewrites every entry — pure functions from type lists to type lists that the compiler evaluates during type checking at no runtime cost. That `finalize_build` type-checks only when every field is present is the compile-time completeness guarantee a closed row gives, recovered for generic Rust: the [application builder](../examples/application-builder.md) example assembles a whole application context this way, with independent providers each contributing one subsystem's fields. + +### Presence markers are presence polymorphism + +CGP tracks field presence with the same per-field flags Rémy's presence polymorphism introduced, made concrete as [`MapType`](../reference/traits/map_type.md) markers on a partial record. The companion type a builder walks carries one marker per field: `IsPresent` stores the value, `IsNothing` stores `()`, and the [optional-and-defaulted-field extensions](../reference/traits/optional_fields.md) add `IsOptional` for a field that may or may not be there; the variant side uses `IsVoid` to mark a case ruled out during extraction. A builder starts at all-`IsNothing`, each `build_field` flips one marker to `IsPresent`, and [`FinalizeBuild`](../reference/traits/has_builder.md) exists only at the all-`IsPresent` configuration, so an incomplete record cannot be finalized. These markers are presence flags in Rémy's exact sense — a per-label record of whether a field is there — lifted to Rust's type level, and the optional-field transforms that rewrite every field's marker at once are presence polymorphism made operational. Where a presence-polymorphic function abstracts over whether a field is present, a CGP builder tracks that presence through the markers and refuses to finalize until it is resolved. + +### An enum is a row-typed sum; upcast and downcast are injection and branching + +The variant side is the exact dual. [`#[derive(HasFields)]`](../reference/traits/has_fields.md) represents an enum as a [`Sum!`](../reference/macros/sum.md) of `Field` entries over the `Either`/`Void` spine, and the [structural casts](../reference/traits/cast.md) implement the paper's variant operations directly. `CanUpcast` lifts a value of a narrow enum into a wider one whose variants are a superset — variant *injection*, always successful because every source case has a home in the target — and `CanDowncast` narrows the other way, the containment-guarded projection: + +```rust +// upcast: variant injection — always succeeds +let wide = FooBar::Foo(1).upcast(PhantomData::); +assert_eq!(wide, FooBarBaz::Foo(1)); + +// downcast: succeeds only for variants the target still has +FooBarBaz::Baz(true).downcast(PhantomData::).ok(); // None — Baz has no home in FooBar +``` + +Branching — the combination predicate on the sum side — is CGP's [extensible visitor](../concepts/extensible-variants.md): the `MatchWithValueHandlers` dispatcher derives one extract-and-handle step per variant from the enum's row and runs them as a pipeline, and because each failed extraction rules out a variant at the type level (marking it the uninhabited `Void`), the final match is provably exhaustive with no wildcard arm. Add a variant without a handler and the code stops compiling — the same static safety OCaml's polymorphic variants give, and CGP's answer to the expression problem the [expression interpreter](../examples/expression-interpreter.md) and [extensible shapes](../examples/extensible-shapes.md) examples work through end to end. In *Data types à la carte* terms, `CanUpcast` is `inj` and `CanDowncast` is `prj` — but where à la carte's `prj` returns a `Maybe` with no exhaustiveness check, CGP's branching consumes the whole sum and proves it covered every case, so the projection that à la carte leaves partial CGP makes total. + +### Providers are the paper's computational evidence + +The one point where CGP lines up with the *theory* more than with any implementing language is Morris and McKinna's insight that a row predicate's evidence "has computational content." In CGP that evidence is concrete and visible: the `HasField` impl a derive generates *is* the projection code, the `CanBuildFrom` recursion *is* the concatenation code, and a wired provider *is* the runtime witness that a context can perform an operation. Where a row-typed language elaborates the evidence invisibly during compilation, CGP surfaces it as ordinary traits and impls a programmer can read — which is the same trade the rest of CGP makes, [bypassing coherence](../concepts/coherence.md) by making the machinery explicit rather than magical. + +## What users like and dislike + +Row polymorphism and extensible data types are loved for the extensibility and reuse they unlock, and the praise is consistent across languages. PureScript users value that a single row-polymorphic function serves many record shapes, which "can greatly reduce refactoring" and lets one "create functions that apply to many different kinds of structures without needing to specify every structure" ([Fowler, "Row Polymorphism without the Jargon"](https://jadon.io/blog/row-polymorphism/)), and that rows drive boilerplate-free JSON decoding and type-safe framework bindings ([Nguyen, "Record Row Type and Row Polymorphism"](https://hgiasac.github.io/posts/2018-11-18-Record-Row-Type-and-Row-Polymorphism.html)). One community rule of thumb captures the sweet spot well: "an extensible record is 'better' than a non-extensible record as a function argument," and "an extensible variant is 'better' than a non-extensible variant as a function result" ([PureScript Discourse](https://discourse.purescript.org/t/when-to-use-extensible-types-when-modeling-a-domain/217)). OCaml's manual makes the parallel case for polymorphic variants: extensibility and inferred, reusable variant types with no central declaration. + +The dislikes cluster around three recurring pains, and CGP shares the first two. The loudest is **error messages**: row unification produces enormous, hard-to-read diagnostics — one PureScript user reported row-unification errors "around 152Kb" that "clog up the terminal," and another found the verbose output "mostly intimidates me" and unhelpful for a simple field typo ([PureScript Discourse](https://discourse.purescript.org/t/upcoming-changes-to-error-messages-for-large-records-rows/3696)). This is the same pain that drove PureScript off effect rows and Gleam off row-typed records. The second is **complexity and cognitive burden**: extensible records are widely seen as a specialist tool — "unless you are developing libraries, or generics and type-level programming, you don't need them" ([Nguyen](https://hgiasac.github.io/posts/2018-11-18-Record-Row-Type-and-Row-Polymorphism.html)) — and one user who modeled a domain with them "kinda regret[ted] it, wishing instead I'd just written separate functions, one for each nominal type" ([PureScript Discourse](https://discourse.purescript.org/t/when-to-use-extensible-types-when-modeling-a-domain/217)). The third is more specific to *pure* structural typing and is one CGP largely avoids: a purely structural function can be called on a semantically wrong value, so that "calling a function works but semantically it doesn't make sense, like finding the `area` of a `Fish`" ([Fowler](https://jadon.io/blog/row-polymorphism/)). OCaml's own manual echoes the safety concern, warning that polymorphic variants "result in a weaker type discipline" and undetected tag typos, recommending core variants "for simple programs" ([OCaml manual](https://ocaml.org/manual/5.4/polyvariant.html)). + +## How CGP compares + +CGP makes a different structural bargain from a row-typed language, and the comparison turns on three axes. On **the type system**, CGP keeps Rust nominal and layers a structural view on top per type, opt-in via a derive, where PureScript, OCaml, and Elm make rows or structural variants a pervasive part of the language. That structural view is *predicate-based* rather than *kind-based* — the Gaster–Jones qualified-types tradition rather than PureScript's built-in `Row` kind — which is exactly why it grafts onto Rust's existing trait solver without any new type-system feature: a `HasField` bound is a row constraint, and constraint resolution is what traits already do. This is also CGP's largest advantage over pure structural typing: because a provider still binds to a *context* it was wired for, the "area of a `Fish`" hazard mostly does not arise — a provider reads a context's fields structurally, but which providers a context has is a nominal, wired decision, so structure grants access without erasing identity. On **inference**, CGP is the weaker tool: a row system infers `{ firstName :: String | r }` and unifies rows automatically, while CGP asks you to derive the shape and name the wiring, with no residual row inferred or carried. On **evidence**, CGP is unusually explicit — the projection, concatenation, and injection code are ordinary traits and impls, not compiler-internal elaboration — which aids reading and debugging at the cost of verbosity. + +The costs are real and should be stated plainly. CGP inherits the very pain that pushed two languages away from rows: a mis-wired or incomplete structural operation surfaces as a long, generated-type-heavy trait error, the CGP-idiom echo of a 152Kb row-unification message. CGP's mitigation is [`check_components!`](../reference/macros/check_components.md), which forces the missing field or variant to be named at the wiring site rather than deep inside a use, but the diagnostics remain heavier than a nominal `match`. CGP also demands more ceremony than an anonymous record: derives, type-level tags, and wiring where a row system would infer everything. Where a program genuinely wants terse anonymous records with full inference, and can pay the error-message cost, a real row system such as PureScript's is the better tool, and reaching for CGP's machinery to emulate it would be over-engineering. Where a program is already in nominal Rust and wants structural field-and-variant access at a few well-chosen points — a builder assembled from independent parts, a visitor over an open set of variants, a cast between sibling enums — CGP delivers row polymorphism's payoff without adopting a new kind system, and pays the complexity only where it is used. + +## Presenting CGP to someone who knows this + +A reader fluent in rows arrives with almost the whole mental model, and the fastest way in is to map the vocabulary directly. A struct's derived `HasFields` shape **is a closed row**; [`Product!`](../reference/macros/product.md) and [`Sum!`](../reference/macros/sum.md) are the **row spines** for records and variants; a `HasField` bound is **row containment** (`ρ₁ ≲ ρ₂`); [`ConcatProduct`](../reference/traits/product_ops.md) and `CanBuildFrom` are **record concatenation** (`ρ₁ ⊙ ρ₂ ~ ρ₃`); `CanUpcast`/`CanDowncast` are **variant injection and branching**; and the extensible visitor is the **expression-problem solution** they already know from polymorphic variants. For a reader who knows the Morris–McKinna paper specifically, the sharpest framing is that CGP is a row system whose *evidence* is first-class and readable: the providers and impls are the computational content the paper says row predicates carry, made into ordinary code. For a reader steeped in Haskell's qualified-types tradition — Gaster–Jones records, *Data types à la carte*, or the `row-types` library — the framing is even more direct: a `HasField` bound is a *has*-predicate, `CanUpcast`/`CanDowncast` are `inj`/`prj`, and CGP is predicate-based rows resolved by the trait solver, which is the mechanism they already reach for; the only shift is that Rust's traits play the role of Haskell's class constraints. + +Two expectations must be corrected up front, because a row-trained reader will assume both and be misled. The first is **inference**. This reader expects CGP to infer `{ name :: String | r }` and unify rows for them, and it does not — CGP has no row variable and no row inference; a provider names its required fields as bounds, and a context's shape comes from a derive, not from unification. Present that not as a missing feature but as the deliberate consequence of staying nominal: because CGP does not unify open rows, it also does not produce the row-unification error messages that drove PureScript off effect rows and Gleam off row-typed records — the failure mode they most resent is designed out of the pervasive case, though it reappears, in CGP's own idiom, as verbose trait errors that [`check_components!`](../reference/macros/check_components.md) localizes. The second is **nominality**. This reader may expect structural typing to mean "any shape-compatible value works anywhere," and CGP deliberately does not: structure grants a provider access to a context's fields, but a context still nominally chooses its providers by wiring, so the "area of a `Fish`" call their own tools permit is not automatically expressible. Frame that as the best of both — structural access where you want reuse, nominal identity where you want safety — and lead with the pitch that lands hardest for this audience: row polymorphism's extensibility, brought to a nominal systems language that never had a row kind, opt-in per type so the complexity and the error-message tax are paid only where the power is used. + +## Sources + +The account of the related work draws on official language documentation, the primary research literature on row types, and cited community writing for sentiment; the CGP snippets are drawn from the knowledge base's [extensible records](../concepts/extensible-records.md), [extensible variants](../concepts/extensible-variants.md), and [casting](../reference/traits/cast.md) documents and verified against current macro behavior. + +- [Wikipedia — Structural type system](https://en.wikipedia.org/wiki/Structural_type_system) and [Nominal type system](https://en.wikipedia.org/wiki/Nominal_type_system) — definitions of structural and nominal typing and their contrast. +- [Wikipedia — Row polymorphism](https://en.wikipedia.org/wiki/Row_polymorphism) — row polymorphism as parametric, row-variable-based structural polymorphism, distinct from structural subtyping, and its absence in Rust. +- [PureScript language reference — Types](https://github.com/purescript/documentation/blob/master/language/Types.md) and [Pursuit — Prim](https://pursuit.purescript.org/builtins/docs/Prim) — rows as unordered labeled collections of kind `Row k`, the `Record` constructor and brace sugar, open-row syntax, and duplicate-label (first-occurrence) semantics. +- [Pursuit — Prim.Row](https://pursuit.purescript.org/builtins/docs/Prim.Row) — the `Cons`, `Union`, `Nub`, and `Lacks` row type classes and what each constrains. +- [purescript-simple-json](https://github.com/justinwoo/purescript-simple-json) — row-reflection-driven, codec-free JSON decoding as a representative row-types application. +- [Wand, "Complete Type Inference for Simple Objects" (LICS 1987)](https://www.ccs.neu.edu/home/wand/papers/wand-lics-87.pdf), [Rémy, "Type checking records and variants…" (POPL 1989)](https://dl.acm.org/doi/10.1145/75277.75284), and [Leijen, "Extensible records with scoped labels" (TFP 2005)](https://www.microsoft.com/en-us/research/publication/extensible-records-with-scoped-labels/) — the row-polymorphism lineage, with Leijen's scoped-labels design the one PureScript adopts, and Rémy's the origin of the presence/absence field flags that CGP's `MapType` markers reproduce. +- [Gaster & Jones, "A Polymorphic Type System for Extensible Records and Variants" (Nottingham NOTTCS-TR-96-3, 1996)](http://web.cecs.pdx.edu/~mpj/pubs/polyrec.html) and [Row polymorphism (HandWiki)](https://handwiki.org/wiki/Row_polymorphism) — the qualified-types / lacks-predicate design of rows, resolved like type-class constraints rather than through a built-in row kind, and the presence/absence flag account; this is the design CGP's `HasField`-bound rows most closely resemble. +- [Swierstra, "Data types à la carte" (JFP 2008)](https://www.cs.tufts.edu/~nr/cs257/archive/wouter-swierstra/DataTypesALaCarte.pdf) ([Wadler's summary](https://wadler.blogspot.com/2008/02/data-types-la-carte.html)) — coproducts of signature functors (`:+:`), the `:<:` sub-signature constraint, and its `inj`/`prj` injection and projection, the constraint-based Haskell solution to the expression problem that mirrors CGP's `Sum!` and `CanUpcast`/`CanDowncast`. +- [Freeman, "An Overview of the PureScript Type System"](https://speakerdeck.com/paf31/an-overview-of-the-purescript-type-system) — the explicit link from PureScript's rows to Leijen's scoped labels. +- [Morris & McKinna, "Abstracting extensible data types: or, rows by any other name" (POPL 2019)](https://researchportal.hw.ac.uk/en/publications/abstracting-extensible-data-types-or-rows-by-any-other-name/) ([ACM DOI 10.1145/3290325](https://dl.acm.org/doi/10.1145/3290325)) and the follow-up [Morris & McKinna, "Generic Programming with Extensible Data Types" (arXiv 2307.08759)](https://arxiv.org/pdf/2307.08759) — row theories as a monoidal generalization, the combination (`⊙`) and containment (`≲`) predicates, record/variant duality, coherence and principal types, and evidence with computational content. +- [Hackage — Data.Row.Records (row-types)](https://hackage.haskell.org/package/row-types-1.0.1.2/docs/Data-Row-Records.html) — the Haskell library implementing the Morris–McKinna framework. +- [Leijen, "Koka: Programming with Row-Polymorphic Effect Types" (MSFP 2014)](https://arxiv.org/pdf/1406.2061) — the application of row polymorphism to *effects*, the third row use alongside records and variants, and the setting where scoped rows capture algebraic effects; developed against CGP in the [algebraic effects](algebraic-effects.md) comparison. +- [OCaml manual — Polymorphic variants](https://ocaml.org/manual/5.4/polyvariant.html) and [Types](https://ocaml.org/manual/5.4/types.html) — polymorphic variants and object row typing, their open/closed bounds, and the documented weaker type discipline. +- [Wadler, "The Expression Problem" (1998)](https://homepages.inf.ed.ac.uk/wadler/papers/expression/expression.txt) — the expression problem and its rows-and-columns framing. +- [TypeScript Handbook — structural typing](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-oop.html) and [Bornea, "Approximating Row Type Polymorphism in TypeScript"](https://medium.com/@gabriel-bornea/approximating-row-type-polymorphism-in-typescript-495ebd5a623d) — structural (duck) typing and its distinction from true row polymorphism. +- [Go specification — Interface types](https://go.dev/ref/spec#Interface_types), [Scala 3 Reference — Structural Types](https://docs.scala-lang.org/scala3/reference/changed-features/structural-types.html), [Elm — Records](https://elm-lang.org/docs/records), and [Andreas, "Structural vs Nominal Typing in Rust"](https://felixandreas.me/blog/nominal-vs-structural-types/) — the wider structural-typing landscape and Rust's nominal baseline. +- [PureScript-Resources, "Why did PureScript go from Eff to Effect?"](https://purescript-resources.readthedocs.io/en/latest/eff-to-effect.html) — the removal of row-typed effects and the role of row-unification errors. +- [Gleam v0.4 release notes](https://gleam.run/news/gleam-v0.4-released/), [GitHub discussion #3223](https://github.com/gleam-lang/gleam/discussions/3223), and [welltypedwitch, "Nominal for storing, structural for manipulating"](https://welltypedwitch.bearblog.dev/nominal-for-storing-structural-for-manipulating/) — Gleam's removal of row-typed records, its nominal stance, and why variant inference is not row polymorphism. +- [Fowler, "Row Polymorphism without the Jargon"](https://jadon.io/blog/row-polymorphism/), [Nguyen, "Record Row Type and Row Polymorphism"](https://hgiasac.github.io/posts/2018-11-18-Record-Row-Type-and-Row-Polymorphism.html), and PureScript Discourse threads on [error messages](https://discourse.purescript.org/t/upcoming-changes-to-error-messages-for-large-records-rows/3696) and [when to use extensible types](https://discourse.purescript.org/t/when-to-use-extensible-types-when-modeling-a-domain/217) — cited community sentiment on what users like and dislike. diff --git a/docs/related-work/type-classes.md b/docs/related-work/type-classes.md new file mode 100644 index 00000000..ca24c44b --- /dev/null +++ b/docs/related-work/type-classes.md @@ -0,0 +1,173 @@ +# Type classes + +Type classes are the mechanism for *principled ad-hoc polymorphism* — a class declares an interface, an instance implements it for a type, and the compiler resolves which instance a constrained function needs by translating the class into a hidden *dictionary* argument. Rust traits are Rust's type classes, so CGP already lives inside a type-class system; its defining move is to escape that system's *coherence* — the one-instance-per-type rule — by making instances first-class values selected explicitly per context, which is exactly the freedom that overlapping and incoherent instances chase in Haskell, Agda, and Lean without ever making it safe. + +## Purpose + +Type classes solve overloading without giving up type inference or static safety. Before them, a name like `show` or `+` either meant one fixed thing or was resolved by unprincipled special-casing in the compiler; type classes, introduced by Philip Wadler and Stephen Blott to "make ad-hoc polymorphism less ad hoc," let a single name be overloaded across many types in a way the type system tracks and the programmer extends ([Wadler & Blott, *How to make ad-hoc polymorphism less ad hoc*](https://dl.acm.org/doi/10.1145/75277.75283)). A function constrained by `Show a =>` works for every type with a `Show` instance, the right instance is found by the compiler, and no instance is chosen by accident. + +This is the comparison closest to CGP's core, because CGP is built on Rust traits and Rust traits *are* type classes. Where the [dependency-injection](dependency-injection.md) and [implicit-parameters](implicit-parameters.md) documents approach CGP from the runtime-container and implicit-value angles, this one meets it on its own ground: a CGP [component](../reference/macros/cgp_component.md) is a class, a [provider](../reference/macros/cgp_impl.md) is an instance, [wiring](../reference/macros/delegate_components.md) is instance resolution, and [impl-side dependencies](../concepts/impl-side-dependencies.md) are class constraints. The single thing CGP changes is coherence, and the whole knowledge base's account of [bypassing coherence](../concepts/coherence.md) is, read against this document, an account of how to have the overlapping and incoherent instances every type-class language struggles to offer safely. + +## The concept in depth + +Type classes have a stable core — classes, instances, constraints, and the dictionary-passing translation — and a large periphery of coherence rules and the extensions that relax them, realized differently across Haskell, Agda, and Lean. The subsections build from the core translation through the coherence property that governs it, the overlapping and incoherent extensions that bend it, the two dependently-typed treatments that abandon it, and the *modular type classes* result that reframes the whole thing in terms of ML modules. + +### Classes, instances, and dictionary passing + +A class declares an interface, an instance implements it for a type, and a constraint requires it — and underneath, the class is a record of functions passed invisibly. In Haskell a class and an instance read as: + +```haskell +class Show a where + show :: a -> String + +instance Show Bool where + show True = "True" + show False = "False" + +describe :: Show a => a -> String +describe x = "value: " ++ show x +``` + +The `Show a =>` constraint on `describe` is the crux. Wadler and Blott's translation compiles a class into a *dictionary* — a record whose fields are the class methods — an instance into a dictionary value, and a constrained function into one that takes the dictionary as an extra hidden argument, so `describe` elaborates to `describe dict x = "value: " ++ show dict x` and the compiler supplies the `Show Bool` dictionary at each call ([Wadler & Blott 1989](https://dl.acm.org/doi/10.1145/75277.75283)). Dictionary passing is the shared implementation model behind every system in this document, and behind CGP: it is the same idea as the evidence passing in the [algebraic effects](algebraic-effects.md) comparison and the qualified-types constraints in the [row polymorphism](row-polymorphism.md) one. Classes also compose through *superclasses* — `class Eq a => Ord a` requires every `Ord` type to be `Eq` — and modern class systems add *associated types* (a type member of a class), which correspond to CGP's [abstract-type components](../concepts/abstract-types.md). + +### Coherence: one instance per type, globally + +The property that governs instance resolution, and the one CGP discards, is *coherence*: for a given class and type there is one instance, and every resolution anywhere in the program finds the same one. Coherence is what makes silent, automatic resolution safe — because the compiler always finds the same `Show Bool`, it can insert the dictionary without the programmer worrying that a different `Show Bool` is chosen elsewhere. The property is usually decomposed into *confluence* (any way of resolving a constraint gives the same dictionary), *coherence* (the program behaves as if there were a canonical instance), and *global uniqueness* (there really is only one instance per type program-wide), which GHC guarantees for the instances in scope during a compilation but does not enforce across a whole program ([Yang, *Type classes: confluence, coherence and global uniqueness*](https://blog.ezyang.com/2014/07/type-classes-confluence-coherence-global-uniqueness/)). Two rules protect it: at most one instance per (class, type), and the *orphan rule* discouraging an instance defined in neither the class's nor the type's module. The canonical benefit is a `Set` of an ordered element type: because there is one `Ord` for that type, values inserted under one ordering and read under another can never disagree, so the set cannot silently corrupt. + +Rust makes the same choice but *enforces* it where Haskell only advises. Rust traits are type classes with a hard orphan rule — an `impl Trait for Type` is allowed only when the crate owns the trait or the type — so coherence is a compile error to violate, not a discouraged convention ([Rust RFC 2451, *re-rebalancing coherence*](https://rust-lang.github.io/rfcs/2451-re-rebalancing-coherence.html); [*Type class*, Wikipedia](https://en.wikipedia.org/wiki/Type_class)). This strictness is precisely the wall CGP is built to get around: the [coherence](../concepts/coherence.md) concept opens on exactly these overlap and orphan rules. + +### Overlapping instances + +The first extension relaxes the one-instance rule to allow instances where one is strictly more specific, letting the compiler pick the most specific match. Haskell exposes this through per-instance pragmas — `{-# OVERLAPPING #-}`, `{-# OVERLAPPABLE #-}`, `{-# OVERLAPS #-}` — that replaced the blunt module-wide `OverlappingInstances` flag in GHC 7.10, so that a general instance and a special case can coexist: + +```haskell +instance {-# OVERLAPPABLE #-} Show a => Show [a] where -- lists in general + show xs = "[" ++ intercalate "," (map show xs) ++ "]" + +instance {-# OVERLAPPING #-} Show [Char] where -- but strings specially + show s = s +``` + +Resolution commits to an instance only when it is strictly more specific than every other match; otherwise the program is rejected as ambiguous ([GHC User's Guide, *Instance declarations and resolution*](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/instances.html)). The cost is fragility: the most-specific heuristic is subtle, and — the point that matters for CGP — GHC's own manual warns that "overlapping instances must be used with care as they can give rise to incoherence (different instance choices are made in different parts of the program) even without `-XIncoherentInstances`." + +### Incoherent instances + +The second extension goes further and lets the compiler commit to an instance even when the choice is not unique, breaking coherence outright. An instance marked `{-# INCOHERENT #-}` may be selected in a context where a more specific instance could later apply, so different parts of a program can resolve the same constraint to different dictionaries. GHC documents the danger plainly: "GHC's optimiser assumes that type-classes are coherent, and hence it may replace any type-class dictionary argument with another dictionary of the same type," which "may cause unexpected results if incoherence occurs," and notes that `INCOHERENT` "still leads to indeterministic behavior and thus should be used with caution" ([GHC User's Guide](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/instances.html)). Incoherent instances are widely regarded as a last resort, because the very automation that makes type classes pleasant — the compiler silently choosing a dictionary — becomes a footgun once the choice is not guaranteed unique. This is the exact hazard CGP confronts head-on, and the reason its embrace of many instances has to be paired with explicit selection. + +### Instance arguments in Agda: coherence dropped, resolution scoped + +Agda offers type-class-style overloading without a class construct and without coherence, through *instance arguments*. Devriese and Piessens's design reuses Agda's ordinary dependently-typed records as classes and marks a resolved argument with double braces, so resolution is "a new type of function argument resolved from call-site scope in a type-directed way" ([Devriese & Piessens, *On the Bright Side of Type Classes: Instance Arguments in Agda*](https://dl.acm.org/doi/10.1145/2034574.2034796)): + +```agda +record Show (A : Set) : Set where + field show : A → String + +instance + showBool : Show Bool + showBool = record { show = λ b → if b then "true" else "false" } + +print : {A : Set} → {{Show A}} → A → String +print {{s}} x = Show.show s x +``` + +Agda does not enforce global uniqueness: instance search succeeds when exactly one instance in the call-site scope matches, and a genuine ambiguity is a *local* error at that use site rather than a program-wide coherence guarantee. This is a scoped, search-based resolution — closer to Scala's implicits than to Haskell's global coherence — and it is the first hint of the design CGP takes further: many instances allowed, the conflict resolved where the choice is made rather than by a global uniqueness rule. + +### Type classes in Lean: search, priorities, and the diamond problem + +Lean uses type classes pervasively for its mathematical library, resolves them by a backtracking tabled search with per-instance priorities, and pays for the absence of coherence with the *diamond problem*. A class is a structure, an instance is registered for search, and resolution finds a value of the required class: + +```lean +class Show (α : Type) where + show : α → String + +instance : Show Bool where + show b := if b then "true" else "false" + +#eval Show.show true +``` + +Because Lean allows multiple instances and orders them by priority, the same constraint can be satisfiable in more than one way, and when two resolution paths to the same class instance disagree — a *diamond*, "the existence of multiple conflicting terms of a class found within the typeclass instance graph" — inference can pick the wrong one or diverge; tabled resolution was introduced partly to tame the exponential blowup diamonds cause ([Selsam, Ullrich & de Moura, *Tabled Typeclass Resolution*](https://arxiv.org/pdf/2001.04301)). In a proof assistant the stakes are higher than a wrong value: two instances that are not *definitionally equal* break proofs that assume they coincide, so the Mathlib community must ensure that any overlapping instances are definitionally equal for every instantiation in the overlap — a standing, laborious discipline of incoherence management ([Baanen, *Use and abuse of instance parameters in the Lean mathematical library*](https://arxiv.org/pdf/2202.01629)). Lean is the clearest cautionary example of what unmanaged instance choice costs, and the sharpest backdrop for CGP's explicit per-context selection, which admits no diamond because a context names exactly one provider per component. + +### Modular type classes: classes as signatures, instances as structures + +The result that unifies this document with the [ML modules](ml-modules.md) comparison is that type classes *are* ML modules plus a resolution layer, established by Dreyer, Harper, and Chakravarty. Their *Modular Type Classes* treats **classes as signatures and instances as structures and functors**, adding a notion of designating certain instance modules as *canonical* within a scope so the compiler can resolve them implicitly, while keeping explicit module linking as the default ([Dreyer, Harper & Chakravarty, *Modular Type Classes*](https://people.mpi-sws.org/~dreyer/papers/mtc/main-long.pdf)). The reframing is illuminating because it separates two things a plain type-class system fuses: the *interface/implementation* structure (which is just modules) and the *canonical implicit resolution* (which is the extra ingredient type classes add on top). + +Separating them also names the tension every design in this document negotiates: **canonicity fights modularity.** Canonicity — one designated instance the compiler may assume everywhere — is what makes implicit resolution safe, but it is fundamentally a *non-modular* property, since a module cannot in general guarantee its instance is the canonical one program-wide, and full modular abstraction lets two modules each supply a different one. Dreyer, Harper, and Chakravarty confine canonicity to a scope to get some of both; OCaml's [modular implicits](ml-modules.md) inherit the same tension and, as their authors concede, cannot fully reconcile canonicity with modular abstraction. The design space is therefore a spectrum. At one end sits Haskell: fully implicit resolution, global coherence, one instance per type, no modularity of choice. In the middle sit modular type classes, modular implicits, Agda instance arguments, and Scala implicits: implicit resolution with canonicity scoped or dropped, paying in ambiguity and search subtleties. At the far end sits CGP: no canonicity at all, no search, explicit per-context selection — the fully modular extreme, where the price of dropping canonicity is that selection must be written down, and the reward is that overlapping and orphan instances become ordinary rather than exceptional. CGP is what the modular-type-classes line of work looks like when canonicity is abandoned entirely rather than merely scoped. + +## How CGP expresses it + +CGP is a type-class system with the coherence rule removed and replaced by explicit per-context instance selection, built by splitting each class into a consumer and a provider side. The consumer trait is an ordinary Rust type class; the provider trait and the wiring are the machinery that make instances first-class values, so many overlapping instances can coexist and a context picks one. Every construct below maps onto a type-class concept, and the extension features that type-class languages bolt on with pragmas are, in CGP, the default. + +### A component is a class; a provider is a first-class instance + +A CGP component is a type class, and a provider is an instance made into a named, selectable value. Declaring a component is declaring a class: + +```rust +#[cgp_component(AreaCalculator)] +pub trait CanCalculateArea { + fn area(&self) -> f64; +} +``` + +`CanCalculateArea` is the class interface, exactly as `class Show a` is. The difference is what an instance may be. A Haskell instance is anonymous and canonical — there is one `Show Bool`, chosen by the compiler — whereas a CGP provider is a named marker type carrying a provider-trait impl, which is precisely a *first-class dictionary*: a value you can name, pass, and choose among. Moving the implementation off the context and onto a provider marker, through the [consumer/provider split](../concepts/consumer-and-provider-traits.md), is the single device that lifts the one-instance-per-type limit — because a provider implements the provider trait for *its own* type, coherence never forbids a second one. Wiring then plays the role of resolution, but explicitly: a [`delegate_components!`](../reference/macros/delegate_components.md) entry names which instance a context uses, where Haskell's compiler would search for the canonical one. A provider's `where` bounds and [`#[uses]`](../reference/attributes/uses.md) imports are the class constraints threaded by dictionary passing, and the context is the dictionary that carries them. + +### Overlapping instances are the default, with no heuristic + +Where Haskell needs pragmas and a most-specific heuristic to permit overlap, CGP permits unlimited overlapping instances by construction and resolves them by naming, not guessing. The [modular serialization](../examples/modular-serialization.md) example defines several providers that all serialize the same types and overlap freely: + +```rust +#[cgp_impl(UseSerde)] +impl ValueSerializer +where + Value: serde::Serialize, +{ /* ... */ } + +#[cgp_impl(SerializeBytes)] +impl ValueSerializer +where + Value: AsRef<[u8]>, +{ /* ... */ } +``` + +A type like `String` matches both, so as vanilla type-class instances one would be rejected and even with `OVERLAPPING` the compiler would need one to be strictly more specific — but as CGP providers both compile, because each implements the provider trait for its own marker. There is no most-specific rule and therefore no ambiguity: a context simply names the provider it wants for a given type, `ValueSerializerComponent: SerializeBytes` or a per-type [`open`](../reference/macros/delegate_components.md) dispatch. The fragility GHC warns about — overlap silently producing incoherence — cannot arise, because the choice is never inferred. + +### Incoherent instances made deterministic and local + +The deepest correspondence is that CGP embraces the very incoherence type-class languages fear, and makes it safe by moving the choice from a global search to an explicit, per-context table. GHC warns that incoherent or overlapping instances let "different instance choices be made in different parts of the program," silently, and that the optimiser may swap one dictionary for another; Lean pays for the same freedom with the diamond problem. CGP allows the same many-instances-for-one-type situation — that is the whole point of [bypassing coherence](../concepts/coherence.md) — but the resolution is neither global nor implicit: each context selects one provider per component in its wiring, so two contexts may resolve the same type differently *on purpose*, and within a single context the choice is unambiguous and fixed. The `AppA` that serializes a `Vec` as hexadecimal and the `AppB` that serializes it as base64 are two coherent local scopes, not a global incoherence — the different-choice-in-different-places that is a footgun in Haskell is a feature in CGP because it is written down rather than inferred. CGP is, in one line, *incoherent instances with the incoherence made deterministic and the selection made explicit.* + +### No orphan rule, no newtype dance + +Two everyday type-class frustrations disappear because a provider's `Self` is always a type its crate owns. The orphan rule — Rust's enforced version of Haskell's convention — never bites, so a downstream crate can supply a provider for a component and a type it did not define, the exact case the [coherence](../concepts/coherence.md) concept shows type classes forbidding. And the newtype trick that Haskell forces when a type needs a second instance (a `Sum` and a `Product` monoid wrapping `Int`) is unnecessary: a second behavior for a type is just a second provider, named directly, with no wrapper type to introduce and unwrap. Where a type-class programmer restructures modules to place an instance or wraps a type to duplicate one, a CGP programmer writes another provider and a wiring line. + +## What users like and dislike + +Type classes are among the most loved features of the languages that have them, and the praise is specific. Programmers value that overloading is *principled* — the compiler tracks and checks it — and *inferred*, so the dictionary is supplied automatically and generic code reads as if monomorphic. They value coherence's payoff of *global uniqueness*: one `Ord` per type means a `Set` or a `Map` cannot be corrupted by mixing orderings, a guarantee that holds without the programmer thinking about it ([Yang 2014](https://blog.ezyang.com/2014/07/type-classes-confluence-coherence-global-uniqueness/)). And they value the *lawful abstractions* type classes make idiomatic — `Functor`, `Monoid`, `Monad` — and the way superclass constraints compose. In dependently-typed settings the same machinery organizes enormous algebraic hierarchies, which is why Lean's Mathlib rests on it. + +The complaints track the coherence rules and the extensions that strain against them. The one-instance-per-type limit forces the *newtype workaround*, which is boilerplate and "breaks down when the type is embedded in another type" ([Yang 2014](https://blog.ezyang.com/2014/07/type-classes-confluence-coherence-global-uniqueness/)); the *orphan rule* forces awkward module structure ([Queensland FP Lab, *Orphan Rules*](https://qfpl.io/posts/orphans-and-fundeps/)); *overlapping instances* are subtle and *incoherent instances* are, by GHC's own account, indeterministic and dangerous ([GHC User's Guide](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/instances.html)). Haskell has no easy *local* or *scoped* instance because local instances reintroduce a coherence problem, which is a long-running community sore point and a place where Scala, Agda, and Lean chose differently. Some practitioners argue the whole coherence bargain is wrong and prefer explicit dictionaries so the choice is visible and local, as Paul Chiusano does in *The trouble with typeclasses* ([Chiusano 2018](https://pchiusano.github.io/2018-02-13/typeclasses.html)); the coherence debate is active enough to have its own recent survey ([Racordon, *On the State of Coherence in the Land of Type Classes*](https://arxiv.org/pdf/2502.20546)). And in Agda and Lean the absence of enforced coherence surfaces as the *diamond problem* and the burden of keeping overlapping instances definitionally equal ([Baanen 2022](https://arxiv.org/pdf/2202.01629)), plus resolution-performance and error-message costs. + +## How CGP compares + +CGP makes the opposite coherence bargain from Haskell and a more disciplined one than Agda or Lean, and the trade is cleanest stated across three axes. On *resolution*, type classes are implicit and CGP is explicit: Haskell finds the dictionary, CGP asks a context to name the provider. On *coherence*, type classes are coherent — globally unique instances, enforced in Rust, conventional in Haskell — while CGP is deliberately incoherent at the definition level, hosting unlimited overlapping and orphan providers. On *safety of the incoherence*, CGP is the disciplined one: where Haskell's `INCOHERENT` and Lean's diamonds let a wrong instance be chosen silently or a proof break, CGP's per-context table makes every choice explicit and local, so incoherence never means indeterminism. Each side pays for what the other gets. Type classes get zero-boilerplate resolution and global uniqueness and pay with the newtype dance, the orphan rule, and the fragility of the overlap extensions; CGP gets many local instances, per-context choice, and freedom from orphans and diamonds, and pays with the wiring — there is no search, so the selection must be written down. + +Neither is better in the abstract, and the honest positioning names where each wins. When a program genuinely wants one canonical instance per type program-wide — one `Ord`, one `Show`, one serialization, so that a `Set` cannot be corrupted and generic code needs no wiring — coherent type classes are the right tool, and simulating that with CGP's per-context wiring would be over-engineering that reintroduces by hand the uniqueness the compiler would give for free. When a program needs several interchangeable instances, per-deployment or per-context choice, instances for types and traits it does not own, or must escape the diamond and orphan problems, CGP's explicit selection is the better tool, and the coherence it discards was the very thing in the way. CGP's explicitness is, for its audience, a feature rather than a cost, in the spirit of the "explicit dictionaries" critics of coherence advocate: the choice is a greppable line in a wiring table, not the outcome of a resolution search that overlap or incoherence could quietly derail. + +## Presenting CGP to someone who knows this + +A reader who knows type classes holds essentially all of CGP's conceptual furniture, and the way in is to state the correspondence and then the one change. A **component is a class**, a **provider is an instance** — but a first-class, named one — **wiring is instance resolution made explicit**, an **impl-side dependency is a class constraint**, the **context is the dictionary** that carries them, and a component's **associated type is a class's associated type**. The one change is coherence: CGP removes the one-instance-per-type rule and, with it, automatic resolution, and puts explicit per-context selection in their place. Framed this way CGP is not a new paradigm to this reader but their own type-class system with coherence swapped for modular, per-context choice — the fully-modular end of the [modular type classes](#modular-type-classes-classes-as-signatures-instances-as-structures) spectrum. + +The expectations to correct are the two coherence buys. First, *automatic resolution*: this reader expects the compiler to find the instance by type, and CGP does not — the provider is named in a table. Present that as the deliberate consequence of the capability they will find most striking, which is that CGP hosts the overlapping and orphan instances their language forbids or makes fragile. Lead with the pains coherence causes them: the newtype wrapper to get a second `Monoid`, the orphan-rule module contortions, the `OVERLAPPING` pragmas that can still go incoherent, the `INCOHERENT` footgun, and — for the Agda or Lean reader — the diamond problem and the definitional-equality drudgery of keeping overlapping instances aligned. Each of these disappears when instances are distinct marker types selected per context, and the pitch that lands is *overlapping and incoherent instances, made safe*: the freedom those extensions reach for, with the indeterminism designed out because the choice is explicit and local rather than searched and global. + +Second, *global uniqueness*: this reader may expect that once a type has an instance, it is the instance everywhere, so a `Set` is safe. CGP deliberately does not promise this program-wide — it promises it *per context*. Say so plainly, because a reader who assumes global uniqueness will look for a guarantee CGP scopes rather than globalizes; then frame the scoping as the point, since it is what lets two contexts serialize the same type two ways without conflict. For the reader who has read *The trouble with typeclasses* or fought a coherence bug, the framing is that CGP is the explicit-dictionary design they wished for, with the ergonomics of a wiring table and the compile-time verification of [`check_components!`](../reference/macros/check_components.md) standing in for the resolution they gave up. + +## Sources + +The account of the related work draws on the primary literature on type classes and their coherence, the official documentation of GHC, Agda, and Lean, and cited community writing for sentiment; the CGP snippets are drawn from the knowledge base's [bypassing coherence](../concepts/coherence.md), [consumer and provider traits](../concepts/consumer-and-provider-traits.md), and [modular serialization](../examples/modular-serialization.md) material and verified against current macro behavior. + +- [Wadler & Blott, *How to make ad-hoc polymorphism less ad hoc* (POPL 1989)](https://dl.acm.org/doi/10.1145/75277.75283) ([PDF](http://users.csc.calpoly.edu/~akeen/courses/csc530/references/wadler.pdf)) — the origin of type classes and the dictionary-passing translation that compiles a class into a record of methods passed as a hidden argument. +- [GHC User's Guide — Instance declarations and resolution](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/instances.html) — the one-instance rule, the orphan-instance rule, the `OVERLAPPING`/`OVERLAPPABLE`/`OVERLAPS`/`INCOHERENT` pragmas, and GHC's own warnings that overlap can give rise to incoherence and that incoherent instances are indeterministic. +- [Yang, *Type classes: confluence, coherence and global uniqueness*](https://blog.ezyang.com/2014/07/type-classes-confluence-coherence-global-uniqueness/) — the decomposition of coherence into confluence, coherence, and global uniqueness, the `Set`/`Ord` safety argument, and the newtype workaround and its breakdown. +- [Rust RFC 2451 — re-rebalancing coherence](https://rust-lang.github.io/rfcs/2451-re-rebalancing-coherence.html) and [*Type class* (Wikipedia)](https://en.wikipedia.org/wiki/Type_class) — Rust traits as type classes with an *enforced* orphan rule, versus Haskell's discouraged-by-convention orphans. +- [Devriese & Piessens, *On the Bright Side of Type Classes: Instance Arguments in Agda* (ICFP 2011)](https://dl.acm.org/doi/10.1145/2034574.2034796) — Agda's instance arguments as call-site-scoped, type-directed resolution over dependently-typed records, without a coherence guarantee. +- [Selsam, Ullrich & de Moura, *Tabled Typeclass Resolution* (Lean)](https://arxiv.org/pdf/2001.04301) and [Baanen, *Use and abuse of instance parameters in the Lean mathematical library*](https://arxiv.org/pdf/2202.01629) — Lean's priority-based backtracking resolution, the diamond problem, and Mathlib's discipline of keeping overlapping instances definitionally equal. +- [Dreyer, Harper & Chakravarty, *Modular Type Classes* (POPL 2007)](https://people.mpi-sws.org/~dreyer/papers/mtc/main-long.pdf) — classes as signatures and instances as structures and functors, scoped canonicity for implicit resolution, and the canonicity-versus-modularity tension that places CGP at the fully-modular extreme. +- [Chiusano, *The trouble with typeclasses*](https://pchiusano.github.io/2018-02-13/typeclasses.html), [Racordon, *On the State of Coherence in the Land of Type Classes*](https://arxiv.org/pdf/2502.20546), and [Queensland FP Lab, *Multi-Parameter Type Classes and their Orphan Rules*](https://qfpl.io/posts/orphans-and-fundeps/) — community sentiment on coherence, the case for explicit dictionaries, and the orphan-instance pain.