Funktional generic type-level programming in Rust: HList, Coproduct, Generic, LabelledGeneric, Validated, Monoid and friends.
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 2 个月前 | ||
| 7 年前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 3 年前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 年前 | ||
| 2 个月前 | ||
| 3 年前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 9 年前 | ||
| 3 年前 | ||
| 3 年前 |
Frunk

frunk frəNGk
- Rust 中的函数式编程工具箱。
- 初看可能有点另类,但你会逐渐喜欢它。
- 来源于:funktional(德语)+ Rust → Frunk
该项目的主要思想是通过在 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);
深入了解的话,以下提供了RustDocs文档资源:
目录
示例
HList
静态类型的异质列表。
首先,让我们启用hlist:
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"));
let t3 = (999, false, "world");
let h3: HList![ isize, bool, &str ] = t3.into();
assert_eq!(h3, hlist![ 999, false, "world" ]);
HLists 提供了一个 hlist_pat! 宏用于模式匹配;
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
在遍历或构建列表时,您还可以在列表前端添加或移除元素:
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);
您可以对它们进行反转、映射以及对折操作:
// 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]);
您可以使用 pluck() 方法从 HList 中提取一个类型,并且在提取该类型后,此方法还会返回剩余部分。这个方法在编译时进行检查,以确保您请求的类型是可以被提取的。
let h = hlist![1, "hello", true, 42f32];
let (t, remainder): (bool, _) = h.pluck();
assert!(t);
assert_eq!(remainder, hlist![1, "hello", 42f32])
同样地,您可以重新塑造或雕刻一个 Hlist,有一个 sculpt() 方法,它允许您通过类型来重新组织或筛选元素。与 pluck() 类似,sculpt() 方法会返回一个包含目标数据和剩余数据对的返回值。此方法在编译时也会进行检查,以确保在运行时不会失败(您请求的目标形状中的类型必须是原始 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 编写代码,您可以编写出抽象于类型和参数数量的函数,但仍然能够在之后恢复原始类型。这可以是一个非常强大的特性。
配置
为了派生 Generic 特性(或 LabelledGeneric),您需要添加 frunk_core 的依赖项。
[dependencies]
frunk_core = { version = "$version" }
Frunk 在开箱即用时提供了一个优雅的 Generic 派生,使得模板代码被减至最少。
以下是一些示例:
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,
});
这也同样适用于另一种情况;只需将一个结构体传递给 into_generic,即可获得它的泛型表示。
结构体之间的转换
有时您可能有两种结构上相同的不同的类型(例如,不同的域但相同的数据)。应用场景包括:
- 您有一个用于从外部API反序列化的模型,以及与您应用逻辑相对应的等价物
- 您希望通过类型来表示同一数据的不同阶段(参见StackOverflow上的这个问题)
泛型提供了一个方便的 convert_from 方法,使得这种转换变得轻松:
// 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
标记泛型
除了 Generic 之外,还有一种 LabelledGeneric,正如其名所示,它依赖于一个带有 标记 的泛型表示。这意味着如果有两个结构体派生自 LabelledGeneric,只有当它们的字段名称相同时,你才能在它们之间进行转换!
以下是一个示例:
// 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);
变换
有时候,您可能拥有两种数据类型,它们在“形状”上相似,但这种相似性是递归的(例如,它包含的字段是结构体,这些结构体的字段是目标类型字段的超集,因此可以进行递归转换)。.transform_from在这种情况下无法帮助您,因为它不处理递归,但如果两者都是通过transmogrify()方法从一种类型转换为另一种类型的LabelledGeneric,那么Transmogrifier就可以提供帮助。
那么,“变换”是什么意思?在此上下文中,它指的是以一种类型安全的方式,递归地将类型A的数据转换为类型B的数据,只要A和B是“形状相似”的。换句话说,只要B的字段及其子字段是A的字段及其相应子字段的子集,那么A就可以转换为B。
像往常一样,Frunk的目标是:
- 使用稳定的(因此没有特化,我认为这本来是有帮助的)
- 类型安全
- 不使用
unsafe
以下是一个示例:
use frunk::labelled::Transmogrifier;
#[derive(LabelledGeneric)]
struct InternalPhoneNumber {
emergency: Option<usize>,
main: usize,
secondary: Option<usize>,
}
#[derive(LabelledGeneric)]
struct InternalAddress<'a> {
is_whitelisted: bool,
name: &'a str,
phone: InternalPhoneNumber,
}
#[derive(LabelledGeneric)]
struct InternalUser<'a> {
name: &'a str,
age: usize,
address: InternalAddress<'a>,
is_banned: bool,
}
#[derive(LabelledGeneric, PartialEq, Debug)]
struct ExternalPhoneNumber {
main: usize,
}
#[derive(LabelledGeneric, PartialEq, Debug)]
struct ExternalAddress<'a> {
name: &'a str,
phone: ExternalPhoneNumber,
}
#[derive(LabelledGeneric, PartialEq, Debug)]
struct ExternalUser<'a> {
age: usize,
address: ExternalAddress<'a>,
name: &'a str,
}
let internal_user = InternalUser {
name: "John",
age: 10,
address: InternalAddress {
is_whitelisted: true,
name: "somewhere out there",
phone: InternalPhoneNumber {
main: 1234,
secondary: None,
emergency: Some(5678),
},
},
is_banned: true,
};
/// Boilerplate-free conversion of a top-level InternalUser into an
/// ExternalUser, taking care of subfield conversions as well.
let external_user: ExternalUser = internal_user.transmogrify();
let expected_external_user = ExternalUser {
name: "John",
age: 10,
address: ExternalAddress {
name: "somewhere out there",
phone: ExternalPhoneNumber {
main: 1234,
},
}
};
assert_eq!(external_user, expected_external_user);
请注意,在撰写本文时,transmogrify() 存在几个已知的局限性,其中一些可能会在将来得到解决:
- 如果其中一个字段是相同类型且派生自
LabelledGeneric,编译器将告诉你它无法为transmogrify()“推断索引”;这是因为Transmogrifier特质的impl将会冲突。这一点在未来可能会改变,也可能不会(也许如果我们转向使用纯过程宏的方式处理问题)? - 对于包含许多需要
transmogrify()的深层嵌套字段的类型,使用此技术可能会增加你的编译时间。 - 如果你在遇到
transform_from的编译时错误(例如,字段缺失)时感到困惑,那么transmogrify()的错误会更糟糕,因为你的类型需要递归地使用transmogrify()。
要了解更多关于泛型和字段的信息,请查看它们各自的 Rust 文档:
路径
LabelledGeneric 派生的结构体可以做的另一件事是使用 Path 及其伴生特质 PathTraverser 进行泛型遍历。在某些圈子中,这个功能也被称为 Lens。
基于 Path 的遍历具有以下特点:
- 通过过程宏
path!(frunk_proc_macros)易于使用- 遍历多层结构很熟悉;只需使用点
.语法(path!(nested.attribute.value))
- 遍历多层结构很熟悉;只需使用点
- 编译时安全
- 可组合(使用
+将一个路径添加到另一个) - 根据传递的内容类型,允许你通过值、引用或可变引用获取
#[derive(LabelledGeneric)]
struct Dog<'a> {
name: &'a str,
dimensions: Dimensions,
}
#[derive(LabelledGeneric)]
struct Cat<'a> {
name: &'a str,
dimensions: Dimensions,
}
#[derive(LabelledGeneric)]
struct Dimensions {
height: usize,
width: usize,
unit: SizeUnit,
}
#[derive(Debug, Eq, PartialEq)]
enum SizeUnit {
Cm,
Inch,
}
let mut dog = Dog {
name: "Joe",
dimensions: Dimensions {
height: 10,
width: 5,
unit: SizeUnit::Inch,
},
};
let cat = Cat {
name: "Schmoe",
dimensions: Dimensions {
height: 7,
width: 3,
unit: SizeUnit::Cm,
},
};
// generic, re-usable, compsable paths
let dimensions_lens = path!(dimensions);
let height_lens = dimensions_lens + path!(height); // compose multiple
let unit_lens = path!(dimensions.unit); // dot syntax to just do the whole thing at once
assert_eq!(*height_lens.get(&dog), 10);
assert_eq!(*height_lens.get(&cat), 7);
assert_eq!(*unit_lens.get(&dog), SizeUnit::Inch);
assert_eq!(*unit_lens.get(&cat), SizeUnit::Cm);
// modify by passing a &mut
*height_lens.get(&mut dog) = 13;
assert_eq!(*height_lens.get(&dog), 13);
还有一个类型级别的宏 Path! 用于声明形状约束。这允许您为 LabelledGeneric 类型编写特定形状的临时函数。
// 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)
);
}
参见 examples/paths.rs 了解其工作原理。
并产物(Coproduct)
如果您曾经希望拥有一种非固定的联合/和类型,而这些类型是您无法控制的,那么您可能想了解一下 Coproduct。在 Rust 中,得益于 enum,您可能在每次需要和使用和类型时声明一个,但通过 Frunk,有一种更轻量级的方法来实现这一点:
use frunk::prelude::*; // for the fold method
// Declare the types we want in our Coproduct
type I32F32Bool = Coprod!(i32, f32, bool);
let co1 = I32F32Bool::inject(3);
let get_from_1a: Option<&i32> = co1.get();
let get_from_1b: Option<&bool> = co1.get();
assert_eq!(get_from_1a, Some(&3));
// None because co1 does not contain a bool, it contains an i32
assert_eq!(get_from_1b, None);
// This will fail at compile time because i8 is not in our Coproduct type
let nope_get_from_1b: Option<&i8> = co1.get(); // <-- will fail
// It's also impossible to inject something into a coproduct that is of the wrong type
// (not contained in the coproduct type)
let nope_co = I32F32Bool::inject(42f64); // <-- will fail
// We can fold our Coproduct into a single value by handling all types in it
assert_eq!(
co1.fold(hlist![|i| format!("int {}", i),
|f| format!("float {}", f),
|b| (if b { "t" } else { "f" }).to_string()]),
"int 3".to_string());
了解更多信息
若需更多信息,请查阅 Coproduct 的文档
Validated
Validated 是一种执行一系列可能出错的操作(例如,返回 Result<T, E> 的函数)的方式,当其中一个或多个操作出错时,能够将所有错误一次性返回给你。如果一切顺利,你将得到包含所有结果的 HList。
映射(或以其他方式处理)普通的 Result 是不同的,因为它会在遇到第一个错误时停止,这在非常常见的情况下(由 Cats 项目 最好地概述)可能会很麻烦。
要使用 Validated,首先:
use frunk::prelude::*; // for Result::into_validated
假设我们定义了一个 Person 结构体
#[derive(PartialEq, Eq, Debug)]
struct Person {
age: i32,
name: String,
street: String,
}
以下是当一切进展顺利时,如何使用它的一个示例。
fn get_name() -> Result<String, Error> { /* elided */ }
fn get_age() -> Result<i32, Error> { /* elided */ }
fn get_street() -> Result<String, Error> { /* elided */ }
// Build up a `Validated` by adding in any number of `Result`s
let validation = get_name().into_validated() + get_age() + get_street();
// When needed, turn the `Validated` back into a Result and map as usual
let try_person = validation.into_result()
// Destructure our hlist
.map(|hlist_pat!(name, age, street)| {
Person {
name: name,
age: age,
street: street,
}
});
assert_eq!(try_person.unwrap(),
Person {
name: "James".to_owned(),
age: 32,
street: "Main".to_owned(),
}));
}
如果另一方面,我们的 Result 存在缺陷:
/// This next pair of functions always return Recover::Err
fn get_name_faulty() -> Result<String, String> {
Result::Err("crap name".to_owned())
}
fn get_age_faulty() -> Result<i32, String> {
Result::Err("crap age".to_owned())
}
let validation2 = get_name_faulty().into_validated() + get_age_faulty();
let try_person2 = validation2.into_result()
.map(|_| unimplemented!());
// Notice that we have an accumulated list of errors!
assert_eq!(try_person2.unwrap_err(),
vec!["crap name".to_owned(), "crap age".to_owned()]);
半群
可以组合的事物。
use frunk::Semigroup;
use frunk::semigroup::All;
assert_eq!(Some(1).combine(&Some(2)), Some(3));
assert_eq!(All(3).combine(&All(5)), All(1)); // bit-wise &&
assert_eq!(All(true).combine(&All(false)), All(false));
半群
可进行组合且具有空值/单位元的元素。
use frunk::monoid::combine_all;
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)
let product_nums = vec![Product(2), Product(3), Product(4)];
assert_eq!(combine_all(&product_nums), Product(24))
特性
Frunk 提供了支持为其核心数据结构派生 serde 序列化器/反序列化器的功能。通过添加 serde 特性标志来启用此功能。
例如,如果您想仅使用 frunk_core 并配合 serde 使用。
[dependencies]
frunk_core = { version = "$version", features = ["serde"] }
或者,如果您想在使用 frunk 时配合 serde 使用,您需要显式地包含 frunk_core。
[dependencies]
frunk = { version = "$version", features = ["serde"] }
frunk_core = { version = "$version", features = ["serde"] }
基准测试
基准测试位于 ./benches 目录下,可以通过以下命令运行:
$ rustup run nightly cargo bench
master 分支上的基准测试也会自动生成、上传并在线可用。
待办事项
稳定接口,通用清理
在发布 1.0 版本之前,最好重新审视接口的设计并进行一些代码(和测试)的通用清理。
尚未实现
鉴于 Rust 不支持高阶类型,我不确定这些是否甚至可以实现。此外,Rustaceans 习惯于在集合上调用 iter() 以获取一个懒视图,使用 map 或 and_then 操作元素,最后使用 collect() 以保持高效。以下结构的实用性在这种情境下可能受限。
FunctorMonadApplyApplicative
贡献
是的,请!
以下内容与 Rust 和函数式编程的精神保持一致,被认为是重要的:
- 安全性(类型和内存)
- 效率
- 正确性
灵感来源
Scalaz、Shapeless、Cats、Haskell,嫌疑犯通常是这些;)
维护者
也就是你可以在 Gitter 上跟踪/标记/@ 的人:)
项目介绍
Rust 中的功能性泛型类型级别编程:HList、Coproduct、Generic、LabelledGeneric、Validated、Monoid 及其相关概念。【此简介由AI生成】