Agentic working instructions for the frunk repository. Read this before making changes. It captures the architecture, the type-level techniques that make the library work, the invariants that must not be broken, and the exact commands CI enforces.
Frunk is a functional programming toolbelt for Rust: HList, Coproduct, Generic, LabelledGeneric, Path (lenses), Validated, Semigroup and Monoid. The library's distinguishing feature is that most of it is implemented with type-level programming on stable Rust: no unsafe, no specialisation, no nightly-only features in the shipped code. Correctness, type/memory safety and efficiency are the stated priorities, in that spirit.
Reference reading (the author's own write-ups, useful for understanding intent, not just mechanics):
- HLists: https://beachape.com/blog/2016/10/23/rust-hlists-heterogenously-typed-list/
- Generic: https://beachape.com/blog/2017/02/04/rust-generic-not-generics/
- LabelledGeneric: https://beachape.com/blog/2017/03/04/labelledgeneric-in-rust-what-why-how/
- Type-level recursion and sculpting: https://beachape.com/blog/2017/03/12/gentle-intro-to-type-level-recursion-in-Rust-from-zero-to-frunk-hlist-sculpting/
- Boilerplate-free struct transforms (transmogrify): https://beachape.com/blog/2017/04/12/boilerplate-free-struct-transforms-in-rust/
- Structural typing (paths): https://beachape.com/blog/2021/05/25/structural-typing-in-rust/
Frunk is a Cargo workspace. Know which crate a change belongs in before editing.
frunk(root,src/): the user-facing facade. Re-exportsfrunk_coreandfrunk_derives, and addsmonoid,semigroup,validated.#![no_std]with anallocfeature.frunk_core(core/): the fundamental building blocks, intentionally minimal. Modules:hlist,coproduct,generic,labelled,path,indices,traits,tuples,macros.#.frunk_derives(derives/): the#[derive(Generic)]and#[derive(LabelledGeneric)]custom derives.proc-macro = true.frunk_proc_macros(proc-macros/): thepath!andPath!function-like proc macros.frunk_proc_macro_helpers(proc-macro-helpers/): shared internals for the two proc-macro crates (type-level label encoding, HList/Coproduct AST builders, field-binding helpers). Not published for direct use.frunk_laws(laws/):quickcheck-based property tests for the algebraic laws (Semigroup,Monoid).
Version note: frunk, frunk_core, frunk_derives share a version; frunk_proc_macros/frunk_proc_macro_helpers and frunk_laws are versioned independently. Path dependencies also carry a version field, so a version bump in one crate must be propagated to its dependents.
These are the exact checks CI (.github/workflows/ci.yml) runs on stable and nightly. Run them locally before proposing a change; a green local run of these is the bar.
# formatting (must be clean)
cargo fmt --all -- --check
# clippy, warnings are errors, including test code
cargo clippy --all --tests -- -D warnings
# type-check and build (std)
cargo check
cargo build
cargo build --examples
# full test suite across the workspace (includes doctests)
cargo test --all --no-fail-fast
# docs must build clean, private items included
RUSTDOCFLAGS='-D warnings' cargo doc --all --no-deps --document-private-itemsno_std is a first-class target and is checked in CI against an embedded target with default features off:
rustup target add thumbv6m-none-eabi
cargo check --target thumbv6m-none-eabi --no-default-features
cargo build --target thumbv6m-none-eabi --no-default-featuresBenchmarks live in benches/ and run with cargo bench (nightly-flavoured harness in CI).
Internalise these five ideas; almost every file is an application of them.
An HList is a heterogeneous, compile-time-typed list, structurally just an arbitrarily nested pair: HCons<H, T> (head + tail) terminated by HNil. Hlist![A, B, C] is sugar for HCons<A, HCons<B, HCons<C, HNil>>>. Construct with hlist![a, b, c], pattern-match with hlist_pat![a, b, c], and write the type with the HList![..] type macro. All the interesting operations (map, foldl/foldr, zip, sculpt, pluck, get, into_reverse, to_ref/to_mut) are traits with a base case for HNil and a recursive case for HCons.
This is the technique that makes the whole library possible on stable Rust. Traits like Selector, Plucker, Sculptor, ByNameFieldPlucker, and Transmogrifier carry an extra Index type parameter that the user never writes; it is solved by type inference. The indices live in core/src/indices.rs: Here (target is the head), There<T> (target is 1 + T deeper in the tail), plus Suffixed, IdentityTransMog, DoTransmog, and several *IndicesWrapper types for transmogrify.
The reason the index exists: without it, the "found it in the head" impl and the "recurse into the tail" impl would overlap, and stable Rust would reject them. Encoding the position into the type signature (Plucker<T, Here> vs Plucker<T, There<TailIndex>>) makes the impls disjoint. When you write a new recursive HList/Coproduct trait, follow this exact pattern: one impl for the head keyed on Here, one for the tail keyed on There<TailIndex> with a where Tail: TheTrait<T, TailIndex> bound.
#[derive(Generic)] gives a type a Repr (an unlabelled HList of its field types) with into/from. The two laws (documented on the trait) are from(into(x)) == x and into(from(y)) == y. Because Repr is purely positional, two structs with the same field types in the same order share a Repr, so convert_from moves between them. This is powerful but positional: it will happily convert between structs whose fields line up by type even if the meaning differs.
#[derive(LabelledGeneric)] produces a Repr of Field<Name, Type> where Name is a type-level string built from zero-sized char enums in frunk_core::labelled::chars (e.g. first_name becomes the tuple type (f, i, r, s, t, __, n, a, m, e)). This makes conversions name-aware:
labelled_convert_fromrequires identical labelledReprs (same names, same order): safer thanGeneric::convert_from.transform_fromusesSculptorto reorder and subset fields by name (drops the remainder).Transmogrifier::transmogrifyrecurses: it converts "similarly shaped" nested structures where the target's fields (and their subfields) are a subset of the source's, mapping throughOption,Box,Vec,VecDeque,LinkedListon the way.
- Coproduct (
core/src/coproduct.rs): a type-levelenum,CNil/Coproduct<H, T>withInl/Inr, the dual of HList.inject,get,take,uninject,subset,embed,fold.#[derive(LabelledGeneric)]on anenumproduces a CoproductRepr. - Path (
core/src/path.rs,path!/Path!macros): lens-like structural typing.path!(a.b.c)builds a value,Path!(a.b.c)the type forwherebounds, both driven byPathTraverser+ByNameFieldPlucker. Lets a function accept "any type shaped likex.y: T" without a bespoke trait.
Breaking any of these is a defect, not a style nit.
- No
unsafe. The library is entirely safe; keep it that way. no_stdcleanliness. Shipped code must compile with--no-default-features. Anything fromalloc(Vec,Box,String,VecDeque,LinkedList, other collections) must sit behind#[cfg(feature = "alloc")]. Thestdfeature is a deprecated alias foralloc; do not add newstd-only code paths. Test modules may usestd(#[cfg(test)] extern crate std;).- Stable Rust, no specialisation. Disambiguate overlapping impls with phantom index types (see idea 2), not
min_specializationor negative impls. Nightly is in the CI matrix only to catch regressions, not to enable features. - Reference symmetry. Operations generally come in owned,
&and&mutflavours. TheLabelledGenericderive emitsLabelledGeneric(owned) plusIntoLabelledGenericfor&and&mut(via the_frunk_ref_lifetime injected byref_generics). HList traits often pair withToRef/ToMutimpls. When adding an operation, consider whether the borrowed variants are also expected. - Base case + recursive case. A new HList trait needs an
HNilimpl and anHCons<H, T>impl; a new Coproduct trait needsCNilandCoproduct<H, T>. Missing the base case gives confusing unresolved-trait errors. - Doctests are the documentation and are tested.
cargo test --allruns every```rustblock;cargo doc -D warningsmust pass. Keep examples compiling and meaningful; they are the primary user-facing docs and are not optional. - serde is opt-in. Serialize/Deserialize derives on data types are gated with
#[cfg(feature = "serde")].
- Match the surrounding style. Self-documenting code over comments; immutability over mutability. Follow existing naming (
THead/TTailfor target,SHead/STailfor source,Index/Indicesfor phantom indices,Reprfor representations). - Diagnostics. Key user-facing traits (
Sculptor,LabelledGeneric,ByNameFieldPlucker,Transmogrifier) carry#[diagnostic::on_unimplemented(..)]to turn inference failures into readable errors. When you add a public trait that users will hit through inference, add one too. - Recursion limit. Deep type-level recursion can exceed the compiler's
recursion_limit(frunk_derivessets#![recursion_limit = "128"]). Do not add gratuitous recursion depth; very large HLists or deep transmogrify chains may force downstream users to raise their own limit. - CHANGELOG. User-visible changes get a
CHANGELOG.mdentry. - Keep the two proc-macro crates in lockstep with core. The derives generate paths like
::frunk_core::labelled::...; renaming or moving a core item breaks generated code that no test infrunk_corealone will catch. Run the whole workspace test suite.
- Add an HList operation (e.g. a new fold/map variant): add the trait to
core/src/hlist.rswithHNil+HConsimpls, expose it as an inherent method onHCons(andHNilif it applies) via thegen_inherent_methods!macro or a dedicatedimpl, and add doctests. ConsiderPoly/Funcsupport andto_ref/to_mutvariants. - Add a recursive, index-driven trait: mirror
Plucker/Sculptor. Add any new phantom index type tocore/src/indices.rs, key the head impl onHere, the tail impl onThere<TailIndex>. - Support a new field-name character in
LabelledGeneric. This requires edits in two places that must stay in sync:- add the char's enum to
create_enums_for! { .. }incore/src/labelled.rs(modulechars); - add the char to
ALPHA_CHARSorUNDERSCORE_CHARSinproc-macro-helpers/src/lib.rs, consistent withencode_as_ident(letters map to themselves;_and digits are prefixed with_; other Unicode is auto-encoded between the_uc/uc_markers). A mismatch produces compile errors only in downstream derived code.
- add the char's enum to
- Add an algebra impl (
Semigroup/Monoidfor a new type): implement insrc/semigroup.rs/src/monoid.rsand add a law test infrunk_laws(laws/src/) usingquickcheck. - Touch the derives: edit
derives/src/derive_generic.rsorderive_labelled_generic.rs; shared token-building lives inproc-macro-helpers/src/lib.rs. Note the derive covers owned +&+&mutimpls and (forLabelledGeneric) both structs and enums;Genericis structs/tuple structs only.
core/src/hlist.rs- HList,Selector,Plucker,Sculptor,HMappable,HFoldLeftable/HFoldRightable,HZippable,IntoReverse,LiftFrom/LiftInto,IntoTuple2.core/src/coproduct.rs-Coproduct/CNiland their traits.core/src/generic.rs-Generic,convert_from,into_generic/from_generic,map_repr/map_inter.core/src/labelled.rs-LabelledGeneric,Field/ValueField,chars,ByNameFieldPlucker,transform_from,Transmogrifier.core/src/path.rs-Path,PathTraverser.core/src/indices.rs- phantom index types (Here,There, ...).core/src/traits.rs-Poly,Func,ToRef,ToMut,IntoReverse.core/src/tuples.rs- tuple <-> HList/Generic interop.core/src/macros.rs-hlist!,hlist_pat!,HList!,Coprod!,field!,poly_fn!.derives/src/-GenericandLabelledGenericderives.proc-macros/src/lib.rs-path!,Path!.proc-macro-helpers/src/lib.rs- label encoding and AST builders shared by the two proc-macro crates.src/{monoid,semigroup,validated}.rs- algebras and theValidatederror accumulator.laws/src/- property tests for the algebraic laws.tests/,examples/,benches/- integration tests, runnable examples, benchmarks.