# Architecture & Design Decisions
This document records architectural decisions and design patterns used in `fp-library`.
For the module layout and dependency graph, see [Project Structure](./project-structure.md).
For the type class hierarchy diagrams, see [Features](./features.md).
## 1. Module Organization
### 1.1. Brand Structs (Centralized)
**Decision:**
Brand structs (e.g., `OptionBrand`) are **centralized** in `src/brands.rs`.
**Reasoning:**
- **Leaf Nodes:** In the dependency graph, Brand structs are **leaf nodes**; they have no outgoing edges (dependencies) to other modules in the crate.
- **Graph Stability:** Centralizing these leaf nodes in `brands.rs` creates a stable foundation. Higher-level modules (like `types/*.rs`) can import from this common sink without creating back-edges or cycles.
### 1.2. Free Functions (Two Layers + Facade)
**Decision:**
Free functions exist in two layers, each adding a level of inference:
1. **`classes/`**: Trait definitions and brand-explicit free functions
without dispatch. Defined in their trait's module (e.g.,
`classes/functor.rs` defines `Functor::map`).
2. **`dispatch/`**: Each dispatch module (e.g., `dispatch/functor.rs`)
contains the complete dispatch system in a `pub(crate) mod inner`:
- The dispatch trait (e.g., `FunctorDispatch`) with Val/Ref impls
that route by closure argument type.
- The inference wrapper (e.g., `pub fn map`) that infers the brand
from the container type via `InferableBrand`.
- A `pub mod explicit` submodule with the brand-explicit dispatch
variant (requires Brand turbofish).
`functions.rs` acts as a **facade**, re-exporting inference wrappers as
the bare names (`map`, `bind`, etc.) and dispatch functions in the
`explicit` sub-module (`explicit::map`, `explicit::bind`, etc.). All
re-exports come from `crate::dispatch::*`.
**Reasoning:**
- **Primary API is inference-based:** Users write `map(f, Some(5))`
with no turbofish. The `explicit::` variants are the escape hatch for
multi-brand types like `Result`.
- **Colocation:** The dispatch trait, its impls, and the inference
wrapper live in the same module. This keeps related code together
and enables the `#[document_module]` proc macro to analyze dispatch
traits and generate HM type signatures for the inference wrappers
in a single pass.
- **Downstream dependencies:** The dependency graph is
`dispatch/ -> classes/`, and `functions.rs` re-exports from
`dispatch/` without adding a layer.
- **Facade pattern:** `functions.rs` re-exports from `dispatch/` modules
to provide a unified API surface without coupling the underlying
definition graph.
## 2. Type Class Hierarchy Design
For the hierarchy diagrams, see [Features](./features.md).
### 2.1. Composite Traits as Blanket Impls
**Decision:**
`Applicative`, `Monad`, `Alternative`, `MonadPlus`, and `Comonad` are
marker traits with blanket implementations over their component traits.
They define no methods of their own.
**Reasoning:**
- **Compositional:** A type becomes `Applicative` automatically by
implementing `Pointed + Semiapplicative + ApplyFirst + ApplySecond`.
No additional code is required.
- **Mirrors theory:** Matches the PureScript/Haskell pattern where
composite classes are defined purely by superclass constraints
(e.g., `class (Pointed f, Semiapplicative f) => Applicative f`).
- **Reduced boilerplate:** Implementors only write the fundamental
operations; composite traits are derived for free.
### 2.2. Micro-Traits (Lift, ApplyFirst, ApplySecond)
**Decision:**
`Lift`, `ApplyFirst`, and `ApplySecond` are separate traits rather than
methods on `Semiapplicative`.
**Reasoning:**
- **Semantic granularity:** `Lift` provides the fundamental operation
(`lift2`, `lift3`, etc.) for lifting uncurried functions into a
functor. `ApplyFirst` and `ApplySecond` are sequencing combinators
with default implementations via `Lift`.
- **Independent supertraits:** `Applicative` requires all four
(`Pointed + Semiapplicative + ApplyFirst + ApplySecond`) as equal
parents. This makes the hierarchy explicit rather than hiding
`ApplyFirst`/`ApplySecond` inside `Semiapplicative`.
- **Overridable defaults:** Types can override `ApplyFirst`/`ApplySecond`
independently for performance without touching `Lift`.
### 2.3. Pointed is Separate from Applicative
**Decision:**
`Pointed` (providing `pure`/`of`) is an independent trait, not part of
`Semiapplicative`.
**Reasoning:**
- **Follows PureScript's hierarchy:** `Pointed` is an independent
superclass of `Applicative`, not bundled with `Apply`.
- **Avoids the "Why not Pointed?" problem:** In Haskell, `pure` is
bundled into `Applicative`, meaning you cannot have `pure` without
`<*>`. Separating them allows types to implement `Pointed` alone
(wrapping values) without committing to `Semiapplicative`.
- **Blanket composition:** Keeping `Pointed` separate makes the
`Applicative` blanket impl a clean intersection of four traits.
### 2.4. Compactable is Separate from Filterable
**Decision:**
`Compactable` (`compact`, `separate`) is a standalone trait.
`Filterable` extends `Compactable + Functor`.
**Reasoning:**
- **Staged capability:** `Compactable` provides the minimal operations
(unwrap nested `Option`s, split `Result`s) without requiring `Functor`.
`Filterable` adds predicate-based operations (`filter`, `partition`,
`filter_map`, `partition_map`) that depend on mapping.
- **Default implementations flow downward:** `filter_map` defaults to
`map` then `compact`; `filter` defaults to `filter_map` with a
predicate. The staging makes these defaults natural.
### 2.5. Ref\* Hierarchy is Independent, Not a Subtrait
**Decision:**
`RefFunctor`, `SendRefFunctor`, etc. are independent traits, not
subtraits of each other or of the base hierarchy.
**Reasoning:**
- **Send/Sync incompatibility:** `ArcLazy::new` requires `Send` on the
closure, which a generic `RefFunctor` cannot guarantee. As a result,
`ArcLazy` implements only `SendRefFunctor` (not `RefFunctor`), and
`RcLazy` implements only `RefFunctor` (not `SendRefFunctor`).
Making `SendRefFunctor: RefFunctor` would prevent `ArcLazy` from
implementing it without also implementing `RefFunctor`, which is
unsound for `Rc`-based internals.
- **Separate from base traits:** By-reference operations return different
types (e.g., `ref_map` returns `Brand::Of<B>` from `&Brand::Of<A>`,
not from an owned value). This is a fundamentally different signature,
not a refinement of `Functor::map`.
### 2.6. Par\* Hierarchy Mirrors the Sequential Hierarchy
**Decision:**
`ParFunctor`, `ParFoldable`, `ParCompactable`, `ParFilterable`, etc.
are separate traits that mirror the sequential hierarchy with
`Send + Sync` bounds throughout.
**Reasoning:**
- **Avoids over-constraining:** Adding `Send + Sync` to the base
hierarchy would prevent non-thread-safe types (anything using `Rc`,
`Cell`, etc.) from implementing `Functor`.
- **Feature-gated execution:** With the `rayon` feature, `par_*`
functions use true parallel execution. Without it, they degrade to
sequential equivalents. The trait hierarchy is identical either way;
only the implementation changes.
- **Composable with Ref axis:** The `ParRef*` variants combine both
the parallel and by-reference axes, giving four combinations total:
base, `Ref*`, `Par*`, `ParRef*`.
## 3. Documentation & Examples
Documentation structure (sections, headings, parameter ordering) is enforced by the documentation macros in `fp-macros/src/documentation/` (`#[document_module]`, `#[document_signature]`, `#[document_type_parameters]`, `#[document_parameters]`, `#[document_returns]`, `#[document_examples]`). All modules should use `#[document_module]`. See `fp-macros/src/lib.rs` for usage.
This section covers content quality guidelines that the macros cannot enforce.
### 3.1. Type Signature Content
The type signature generated by `#[document_signature]` must accurately reflect the code. Contributors should verify:
- Signatures correctly indicate uncurried semantics (the library's standard).
- Brand type parameters are replaced with their corresponding concrete types for clarity. Write `Result e` not `ResultWithErrBrand e`.
- Quantifiers are accurate, correctly ordered (matching the code), and omit unused variables for clarity.
### 3.2. Example Content
Examples should demonstrate the library's intended usage patterns:
- Import items using grouped wildcards (`use fp_library::{brands::*, functions::*}`) instead of individually by name.
- For types with a single unambiguous brand (Option, Vec, Identity, etc.), use inference-based free functions without turbofish: `map(|x| x * 2, Some(5))`.
- For types with multiple brands (Result at arity 1, Tuple2, Pair), use `explicit::` variants with turbofish: `explicit::map::<ResultErrAppliedBrand<E>, _, _, _, _>(f, x)`.
- Prefer free functions over trait method calls (`OptionBrand::map(...)`).
**Reasoning:** The library is designed to be used via free functions with brand inference for the common single-brand case. The `explicit::` variants are the escape hatch for ambiguous types. Examples should demonstrate the inference-based API as the primary path.
## 4. Lint Policy
### 4.1. Restriction Lints
The workspace enables several clippy restriction lints as warnings (promoted to errors by `-D warnings` in CI):
- `clippy::unwrap_used`, `clippy::expect_used` - panicking unwrap/expect
- `clippy::indexing_slicing` - panicking index/slice
- `clippy::panic`, `clippy::todo`, `clippy::unimplemented`, `clippy::unreachable` - explicit panics
These lints are appropriate for production library code but overly strict for test and benchmark code. Test code suppresses them with `#[expect(...)]` rather than `#[allow(...)]`.
### 4.2. `#[expect]` vs `#[allow]`
**Decision:** Use `#[expect(...)]` everywhere the suppressed lint is known to fire. Reserve `#[allow(...)]` only where the lint does not currently fire but suppression is kept for correctness (e.g., `dead_code` on items consumed by macro expansion, `deprecated` on modules testing deprecation-based warnings).
**Reasoning:** `#[expect]` warns when the suppression becomes unnecessary (the lint no longer fires), preventing stale attributes from accumulating. `#[allow]` is silent when unused, so stale `#[allow]` attributes persist undetected.
### 4.3. Reasons on All Lint Attributes
**Decision:** Every `#[expect(...)]` and `#[allow(...)]` attribute must include a `reason = "..."` string.
**Reasoning:** Reasons make the intent self-documenting. Without them, a reader must infer why the lint was suppressed, which is error-prone for restriction lints where the suppression could be masking a real bug.
### 4.4. Suppression Scope
- **Inline test modules** (`#[cfg(test)] mod tests`): Place `#[expect(...)]` on the `mod tests` block, listing only the restriction lints that actually fire within that module.
- **Integration test files** (`tests/*.rs`): Use `#![expect(...)]` as an inner attribute at the file top.
- **Benchmark modules**: Use `#[expect(...)]` on the module declaration in `benchmarks.rs`. Benchmarks that intentionally use identity operations or `and_then` instead of `map` (for fair std-vs-fp comparison) suppress `identity_op`, `bind_instead_of_map`, etc.
- **Production code**: Suppress on the narrowest scope possible (individual statement or function), with a comment or reason explaining the safety invariant.