Baike.dev
All toolsOnline toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
F

frunk

> 编程语言
Open source

Funktional generic type-level programming in Rust: HList, Coproduct, Generic, LabelledGeneric, Validated, Monoid and friends.

1.5K stars0 likes0 views
WebsiteGitHub

About

Funktional generic type-level programming in Rust: HList, Coproduct, Generic, LabelledGeneric, Validated, Monoid and friends.

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:

rust
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

  1. HList
  2. Generic
    • 2.1 LabelledGeneric
      • 2.1.2 Path (Lenses)
  3. Coproduct
  4. Validated
  5. Semigroup
  6. Monoid
  7. Features
  8. Benchmarks
  9. Todo
  10. Contributing
  11. Inspirations
  12. Maintainers

Examples

HList

Statically typed heterogeneous lists.

First, let's enable hlist:

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

Some basics:

rust
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;

rust
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:

rust
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:

rust
// 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.

rust
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.

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]);

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

toml
[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

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);
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:

rust
// 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:

…
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 this:

  • Using stable (so no specialisation, which would have been helpful, methinks)
  • Typesafe
  • No usage of unsafe

Here is an example:

…

Note that as of writing, there are a couple of known limitations with transmogrify(), some of which may be addressed in the future:

  • If one of the fields is an identical type and derives LabelledGeneric, the compiler will tell you that it can't "infer an index" for transmogrify(); this is because impls of the Transmogrifier trait will clash. This may or may not change in the future (perhaps if we move to a pure procedural macro powered way of doing things?)
  • For types that contain many multiple deeply-nested fields that require transmogfiy()ing, using this technique will likely increase your compile time.
  • If you've balked at the the compile-time errors with transform_from when a transform is deemed impossible (e.g. missing field), the errors for transmogrify() are worse to the degree that recursive transmogrify() is required for your types.

For more information how Generic and Field work, check out their respective Rustdocs:

  • Generic
  • Labelled

Path

One of the other things that LabelledGeneric-deriving structs can do is be generically traversed using Path and its companion trait PathTraverser. In some circles, this functionality is also called a Lens.

Path-based traversals are

  • Easy to use through the procedural macro path! (frunk_proc_macros)
    • Traversing multiple levels is familiar; just use dot . syntax (path!(nested.attribute.value))
  • Compile-time safe
  • Composable (add one to the other using +)
  • Allows you to get by value, get by reference or get by mutable reference, depending on the type of thing you pass it.
…

There's also a Path! type-level macro for declaring shape-constraints. This allows you to write adhoc shape-dependent functions for LabelledGeneric types.

rust
// Prints height as long as `A` has the right "shape" (e.g.
// has `dimensions.height: usize` and `dimension.unit: SizeUnit)
fn print_height<'a, A, HeightIdx, UnitIdx>(obj: &'a A) -> ()
where
    &'a A: PathTraverser<Path!(dimensions.height), HeightIdx, TargetValue = &'a usize>
        + PathTraverser<Path!(dimensions.unit), UnitIdx, TargetValue = &'a SizeUnit>,
{
    println!(
        "Height [{} {:?}]",
        path!(dimensions.height).get(obj),
        path!(dimensions.unit).get(obj)
    );
}

See examples/paths.rs to see how this works.

Coproduct

If you've ever wanted to have an adhoc union / sum type of types that you do not control, you may want to take a look at Coproduct. In Rust, thanks to enum, you could potentially declare one every time you want a sum type to do this, but there is a light-weight way of doing it through Frunk:

…

For more information, check out the docs for Coproduct

Validated

Validated is a way of running a bunch of operations that can go wrong (for example, functions returning Result<T, E>) and, in the case of one or more things going wrong, having all the errors returned to you all at once. In the case that everything went well, you get an HList of all your results.

Mapping (and otherwise working with plain) Results is different because it will stop at the first error, which can be annoying in the very common case (outlined best by the Cats project).

To use Validated, first: ``

Issues· 40 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

Rustcoproductdatatype-generic-programmingfpgeneric

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言