# Frunk: a functional programming toolbelt where HLists and Generic do the heavy lifting

> Frunk is a Rust crate of type-level programming tools, where HList gives you heterogeneous lists, Generic lets a function abstract over a type's shape and recover it afterwards, and LabelledGeneric adds lenses over struct fields.

**lloydmeta/frunk** — Funktional generic type-level programming in Rust: HList, Coproduct, Generic, LabelledGeneric, Validated, Monoid and friends.

- Repository: https://github.com/lloydmeta/frunk
- Website: https://beachape.com/frunk/
- Stars: 1,461 · Forks: 73
- Language: Rust
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/lloydmeta-frunk

## The pitch in one example

Frunk describes itself as a functional programming toolbelt in Rust, and the name comes from funktional in German plus Rust. The opening example is the quickest way to see what the crate is for.

```rust
use frunk::monoid::combine_all;

let v = vec![Some(1), Some(3)];
assert_eq!(combine_all(&v), Some(4));
```

And then the one the project calls slightly more magical:

```rust
let t1 =       (1, 2.5f32,                String::from("hi"),  Some(3));
let t2 =       (1, 2.5f32,            String::from(" world"),     None);
let t3 =       (1, 2.5f32,         String::from(", goodbye"), Some(10));
let tuples = vec![t1, t2, t3];

let expected = (3, 7.5f32, String::from("hi world, goodbye"), Some(13));
assert_eq!(combine_all(&tuples), expected);
```

Three tuples of the same shape get combined element by element: the integers add to three, the floats add to 7.5, the strings concatenate in order, and the Options add to Some(13). The strings concatenating rather than adding is the giveaway that this is not arithmetic but a Monoid combine, where the monoid for a string is concatenation, for an integer is addition, and for an Option is the sum of the contained values with None acting as the identity.

That capability is the reason the crate exists. Rust's standard library has no way to say combine a Vec of tuples, because the tuples have no trait bound that permits element-wise combination. Frunk supplies that, and the rest of the crate is the machinery for heterogeneous data, which is what a tuple of mixed types is.

The crate is on crates.io as frunk, the licence is MIT, the documentation is on docs.rs, and the author is Lloyd. The current version is 0.5.0, released on 2026-07-04, after 0.4.4 in June 2025 and 0.4.3 in August 2024. The last push to the repository was on 2026-08-01.

## HList, and the operations that come with it

HList is the statically typed heterogeneous list, the foundational type in the crate. It is built from HNil and HCons, and the hlist! macro builds one.

```rust
use frunk::{HNil, HCons, hlist};

let h = hlist![1];
// Type annotations for HList are optional. Here we let the compiler infer it for us
// h has a static type of: HCons<i32, HNil>

// HLists have a head and tail
assert_eq!(hlist![1].head, 1);
assert_eq!(hlist![1].tail, HNil);

// You can convert a tuple to an HList and vice-versa
let h2 = hlist![ 42f32, true, "hello" ];
let t: (f32, bool, &str) = h2.into();
assert_eq!(t, (42f32, true, "hello"));
```

The comment about the static type being HCons<i32, HNil> is the point of the whole thing. A Vec holds one type, so a Vec of mixed values is impossible, and a tuple's shape is fixed at its type. An HList encodes the shape in the type itself, as a chain of HCons ending in HNil, so each element keeps its own type and the compiler can tell you what you have.

The operations on it are extensive, and two of them stand out because they have no direct Rust equivalent.

pluck() extracts a type out of an HList and gives you back the remainder, and the extraction is checked at compile time so you cannot ask for a type that is not there:

```rust
let h = hlist![1, "hello", true, 42f32];
let (t, remainder): (bool, _) = h.pluck();
assert!(t);
assert_eq!(remainder, hlist![1, "hello", 42f32])
```

And sculpt() is the same idea with a target shape, reordering and culling elements by type, also checked at compile time, with the requirement that the types in the requested target shape be a subset of the types in the original HList:

```rust
let h = hlist![9000, "joe", 41f32, true];
let (reshaped, remainder): (HList![f32, i32, &str], _) = h.sculpt();
assert_eq!(reshaped, hlist![41f32, 9000, "joe"]);
assert_eq!(remainder, hlist![true]);
```

Beyond that there is the standard functional set: reverse, map, and fold, with foldl and foldr both present. Pattern matching gets a macro, hlist_pat!, so you can destructure an HList into named bindings the way you would a tuple, and the HList! type macro writes the nested HCons type as a readable sequence.

Traversal and construction both work at the front, with pop() returning a head and a tail and prepend() putting an element back, and there is macro sugar for both, since a hlist_pat! with a rest pattern is equivalent to pop and the matching hlist! construction is equivalent to prepend.

## Generic and LabelledGeneric, and the derive that pays for them

Generic is a way of representing a type in a generic way. By coding around Generic you can write functions that abstract over types and arity, and still have the ability to recover your original type afterwards, which the project describes as a fairly powerful thing.

The setup requires the frunk_core dependency, because the derive macro lives in a separate crate:

```toml
[dependencies]
frunk_core = { version = "$version" }
```

The main use case is converting between a struct and its generic representation:

```rust
#[derive(Generic, Debug, PartialEq)]
struct Person<'a> {
    first_name: &'a str,
    last_name: &'a str,
    age: usize,
}

let h = hlist!("Joe", "Blow", 30);
let p: Person = frunk::from_generic(h);
```

And the other direction, into_generic, works the same way. The conversion between two structurally identical structs is the case the project highlights, with the use cases of a deserialisation model and an application logic model, or different stages of the same data represented as different types, and a convert_from method to make that painless.

LabelledGeneric is the variant that adds labels to the fields, and with labels comes Path, which the documentation calls lenses. A labelled representation keeps the field names, so you can address a field by name rather than by position, and a path is a lens onto one of those fields. The examples directory makes the distinction concrete with examples/generic.rs, examples/labelled.rs and examples/paths.rs, and the paths example declares a required feature of proc-macros in the Cargo manifest, which is part of the default feature set.

The derives live in a separate crate, frunk_derives, and the proc macro implementation in frunk_proc_macros, with frunk_core holding the runtime pieces. That split is what lets the crate stay no_std friendly, since the core can be built without the compiler-side machinery.

## The workspace, the feature flags, and the benchmark profile

The Cargo manifest shows how the crate is put together, and the workspace list is the clearest picture of the internal structure:

```toml
[workspace]
members = [
    "proc-macro-helpers",
    "core",
    "proc-macros",
    "derives",
    "laws"
]
```

Five member crates. The core is the runtime, the proc-macros crate holds the compiler-side code, derives is what you import to get the derive attributes, proc-macro-helpers is the shared support code, and laws is a separate crate of property-law tests, which is the Haskell-style approach to testing algebra laws such as associativity and identity.

The feature flags are where the design decisions are visible:

```toml
[features]
default = ["validated", "proc-macros", "alloc"]
validated = ["alloc"]
proc-macros = ["frunk_proc_macros"]
std = ["alloc", "serde?/std"]
alloc = ["frunk_core/alloc", "serde?/alloc"]
```

The default set enables validated, proc-macros and alloc. The std feature pulls in alloc, and alloc pulls in the core's alloc feature and serde's alloc feature, which is how the crate supports no_std. Serde is an optional dependency in its own right, enabled through the serde?/std style syntax, so it only appears when something else turns it on.

The bench profile is tuned specifically, and the comment explains why. opt-level is 3, lto is off, debug-assertions is off, and codegen-units is 1 with a link to a users.rust-lang.org thread about odd benchmark results caused by compiler optimisation. Setting codegen-units to 1 makes the benchmark numbers reproducible at the cost of build speed, which is the right trade when the numbers are the point.

The dev dependencies include time at 0.3 and frunk_laws from the laws workspace member, and there is a benches directory plus a dependency-graph.puml file at the root, which is a PlantUML diagram of the internal dependency structure.

## What the crate costs you

The first limitation is compile time and type complexity, and it is inherent rather than a bug. Every HList is a nested chain of HCons types, so a long HList produces a long type, and the derive on every struct with a Generic implementation adds a proc macro pass to the build. The benchmark profile's codegen-units of 1 is a hint that the maintainer cares about the difference between an optimised and an unoptimised build, which matters for a crate whose whole selling point is compile-time work.

The second limitation is that the abstraction has to be earned. A single struct with a single function is better served by plain Rust, and adding a Generic derive to every type in a codebase so that one helper can work over all of them is a trade most projects should make only when they have several such types.

The third limitation is the lens feature dependency. The paths example requires the proc-macros feature, so a build that disables default features loses the labelled path machinery, and the README's table of contents lists Path under LabelledGeneric, meaning the two go together. If you need lenses and have turned off proc-macros, you are in an unsupported configuration.

The fourth is the version line. The crate is at 0.5.0, which in Cargo means the API is not considered stable, and the semantic version history is short and irregular, with 0.4.3 in August 2024, 0.4.4 in June 2025 and 0.5.0 in July 2026. A crate you are considering for a long-lived project should expect to absorb breaking changes on a minor version bump.

On maintenance, the repository is not archived and the last push was on 2026-08-01, with 0.5.0 released on 2026-07-04, so work is recent. The project publishes RustDoc for both the master branch at beachape.com/frunk and the latest release at docs.rs/frunk, and the README links an introduction video.

## Conclusion

Frunk suits a Rust developer who needs to write one function over many struct shapes, or who has a data type that changes shape at runtime, and who is comfortable with type-level machinery and a derive macro on their structs. It is a poor fit for an ordinary application that has one struct and one function, since the abstraction cost is not free, and it is a poor fit if you cannot live with a compile-time cost per derived type. Before adopting, decide whether you need the plain Generic or the labelled variant, since the latter is what gives you lenses and the former is what you want for structural conversion, and read the features list in the README to see which of the parts you actually need are behind a cargo feature flag.

## FAQ

### What is Frunk and what does it provide?

Frunk is a Rust crate of functional programming tools: HList for statically typed heterogeneous lists, Generic and LabelledGeneric for abstracting over a type's shape, Coproduct, Validated, Semigroup and Monoid. The crate is MIT licensed and published on crates.io as frunk.

### How do I enable the HList support in Frunk?

You import the types and the macro: use frunk::{HNil, HCons, hlist}. The hlist! macro builds a list whose static type is a nested chain of HCons ending in HNil, and you can convert a tuple into an HList and back with into().

### What is the difference between Generic and LabelledGeneric in Frunk?

Generic represents a type in a generic way so functions can abstract over types and arity and still recover the original type. LabelledGeneric adds field labels, which is what makes Path, the lens type, possible for addressing struct fields by name. The examples directory has generic.rs, labelled.rs and paths.rs for each.

### How do I install the Frunk derive macros?

Add frunk_core to your dependencies, since the derive lives in a separate crate: frunk_core = { version = "$version" }. The manifest also lists frunk_derives and the optional frunk_proc_macros, and the proc-macros feature flag pulls the proc macro crate in.

### Can Frunk be used without the standard library?

Yes. The feature flags separate alloc from std, with std pulling in alloc, and alloc pulling in the core's alloc feature and serde's alloc feature. The default feature set is validated, proc-macros and alloc, so the crate can be built for a no_std target with the std feature left off.

## Sources

- [License: MIT](https://github.com/lloydmeta/frunk/blob/master/LICENSE)
- [lloydmeta/frunk on GitHub](https://github.com/lloydmeta/frunk)
- [Project website](https://beachape.com/frunk/)
- [README](https://github.com/lloydmeta/frunk/blob/master/README.md)
- [Releases](https://github.com/lloydmeta/frunk/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lloydmeta-frunk
