Frunk
Funktional generic type-level programming in Rust: HList, Coproduct, Generic, LabelledGeneric, Validated, Monoid and friends.
Install / Use
/learn @lloydmeta/FrunkREADME
Frunk

frunk frəNGk
- Functional programming toolbelt in Rust.
- Might seem funky at first, but you'll like it.
- Comes from: funktional (German) + Rust → Frunk
The general idea is to make things easier by providing FP tools in Rust to allow for stuff like this:
use frunk::monoid::combine_all;
let v = vec![Some(1), Some(3)];
assert_eq!(combine_all(&v), Some(4));
// Slightly more magical
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);
For a deep dive, RustDocs are available for:
- Code on Master
- Latest published release
Table of Contents
- HList
- Generic
- 2.1 LabelledGeneric
- 2.1.2 Path (Lenses)
- 2.1 LabelledGeneric
- Coproduct
- Validated
- Semigroup
- Monoid
- Features
- Benchmarks
- Todo
- Contributing
- Inspirations
- Maintainers
Examples
HList
Statically typed heterogeneous lists.
First, let's enable hlist:
use frunk::{HNil, HCons, hlist};
Some basics:
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"));
let t3 = (999, false, "world");
let h3: HList![ isize, bool, &str ] = t3.into();
assert_eq!(h3, hlist![ 999, false, "world" ]);
HLists have a hlist_pat! macro for pattern matching;
let h: HList!(&str, &str, i32, bool) = hlist!["Joe", "Blow", 30, true];
// We use the HList! type macro to make it easier to write
// a type signature for HLists, which is a series of nested HCons
// h has an expanded static type of: HCons<&str, HCons<&str, HCons<i32, HCons<bool, HNil>>>>
let hlist_pat!(f_name, l_name, age, is_admin) = h;
assert_eq!(f_name, "Joe");
assert_eq!(l_name, "Blow");
assert_eq!(age, 30);
assert_eq!(is_admin, true);
// You can also use into_tuple2() to turn the hlist into a nested pair
To traverse or build lists, you can also prepend/or pop elements at the front:
let list = hlist![true, "hello", Some(41)];
// h has a static type of: HCons<bool, HCons<&str, HCons<Option<{integer}>, HNil>>>
let (head1, tail1) = list.pop();
assert_eq!(head1, true);
assert_eq!(tail1, hlist!["hello", Some(41)]);
let list1 = tail1.prepend(head1);
assert_eq!(list, list1);
// or using macro sugar:
let hlist_pat![head2, ...tail2] = list; // equivalent to pop
let list2 = hlist![head2, ...tail2]; // equivalent to prepend
assert_eq!(list, list2);
You can reverse, map, and fold over them too:
// Reverse
let h1 = hlist![true, "hi"];
assert_eq!(h1.into_reverse(), hlist!["hi", true]);
// Fold (foldl and foldr exist)
let h2 = hlist![1, false, 42f32];
let folded = h2.foldr(
hlist![
|acc, i| i + acc,
|acc, _| if acc > 42f32 { 9000 } else { 0 },
|acc, f| f + acc
],
1f32
);
assert_eq!(folded, 9001)
// Map
let h3 = hlist![9000, "joe", 41f32];
let mapped = h3.map(hlist![
|n| n + 1,
|s| s,
|f| f + 1f32]);
assert_eq!(mapped, hlist![9001, "joe", 42f32]);
You can pluck a type out of an HList using pluck(), which also gives you back the remainder after plucking that type
out. This method is checked at compile-time to make sure that the type you ask for can be extracted.
let h = hlist![1, "hello", true, 42f32];
let (t, remainder): (bool, _) = h.pluck();
assert!(t);
assert_eq!(remainder, hlist![1, "hello", 42f32])
Similarly, you can re-shape, or sculpt, an Hlist, there is a sculpt() method, which allows you to re-organise and/or
cull the elements by type. Like pluck(), sculpt() gives you back your target with the remainder data in a pair. This
method is also checked at compile time to make sure that it won't fail at runtime (the types in your requested target shape
must be a subset of the types in the original HList.
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]);
Generic
Generic is a way of representing a type in ... a generic way. By coding around Generic, you can to write functions
that abstract over types and arity, but still have the ability to recover your original type afterwards. This can be a fairly powerful thing.
Setup
In order to derive the trait Generic (or LabelledGeneric) you will have to add frunk_core dependency
[dependencies]
frunk_core = { version = "$version" }
Frunk comes out of the box with a nice custom Generic derivation so that boilerplate is kept to a minimum.
Here are some examples:
HList ⇄ Struct
#[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);
assert_eq!(p,
Person {
first_name: "Joe",
last_name: "Blow",
age: 30,
});
This also works the other way too; just pass a struct to into_generic and get its generic representation.
Converting between Structs
Sometimes you may have 2 different types that are structurally the same (e.g. different domains but the same data). Use cases include:
- You have a models for deserialising from an external API and equivalents for your app logic
- You want to represent different stages of the same data using types (see this question on StackOverflow)
Generic comes with a handy convert_from method that helps make this painless:
// Assume we have all the imports needed
#[derive(Generic)]
struct ApiPerson<'a> {
FirstName: &'a str,
LastName: &'a str,
Age: usize,
}
#[derive(Generic)]
struct DomainPerson<'a> {
first_name: &'a str,
last_name: &'a str,
age: usize,
}
let a_person = ApiPerson {
FirstName: "Joe",
LastName: "Blow",
Age: 30,
};
let d_person: DomainPerson = frunk::convert_from(a_person); // done
LabelledGeneric
In addition to Generic, there is also LabelledGeneric, which, as the name implies, relies on a generic representation
that is labelled. This means that if two structs derive LabelledGeneric, you can convert between them only if their
field names match!
Here's an example:
// Suppose that again, we have different User types representing the same data
// in different stages in our application logic.
#[derive(LabelledGeneric)]
struct NewUser<'a> {
first_name: &'a str,
last_name: &'a str,
age: usize,
}
#[derive(LabelledGeneric)]
struct SavedUser<'a> {
first_name: &'a str,
last_name: &'a str,
age: usize,
}
let n_user = NewUser {
first_name: "Joe",
last_name: "Blow",
age: 30
};
// Convert from a NewUser to a Saved using LabelledGeneric
//
// This will fail if the fields of the types converted to and from do not
// have the same names or do not line up properly :)
//
// Also note that we're using a helper method to avoid having to use universal
// function call syntax
let s_user: SavedUser = frunk::labelled_convert_from(n_user);
assert_eq!(s_user.first_name, "Joe");
assert_eq!(s_user.last_name, "Blow");
assert_eq!(s_user.age, 30);
// Uh-oh ! last_name and first_name have been flipped!
#[derive(LabelledGeneric)]
struct DeletedUser<'a> {
last_name: &'a str,
first_name: &'a str,
age: usize,
}
// This would fail at compile time :)
let d_user: DeletedUser = frunk::labelled_convert_from(s_user);
// This will, however, work, because we make use of the Sculptor type-class
// to type-safely reshape the representations to align/match each other.
let d_user: DeletedUser = frunk::transform_from(s_user);
Transmogrifying
Sometimes you need might have one data type that is "similar in shape" to another data type, but it
is similar recursively (e.g. it has fields that are structs that have fields that are a superset of
the fields in the target type, so they are transformable recursively). .transform_from can't help you
there because it doesn't deal with recursion, but the Transmogrifier can help if both are LabelledGeneric
by transmogrify()ing from one to the other.
What is "transmogrifying"? In this context, it means to recursively tranform some data of type A into data of type B, in a typesafe way, as long as A and B are "similarly-shaped". In other words, as long as B's fields and their subfields are subsets of A's fields and their respective subfields, then A can be turned into B.
As usual, the goal with Frunk is to do t
Related Skills
himalaya
337.3kCLI to manage emails via IMAP/SMTP. Use `himalaya` to list, read, write, reply, forward, search, and organize emails from the terminal. Supports multiple accounts and message composition with MML (MIME Meta Language).
node-connect
337.3kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
83.2kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
coding-agent
337.3kDelegate coding tasks to Codex, Claude Code, or Pi agents via background process
