25 个版本
0.5.0 | 2024年2月11日 |
---|---|
0.4.2 | 2023年6月17日 |
0.4.1 | 2022年11月3日 |
0.4.0 | 2021年6月25日 |
0.0.1 | 2017年3月24日 |
#412 in 数据结构
在 frunk 中使用
78KB
1.5K SLoC
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
- 泛型
- 2.1 LabelledGeneric
- 2.1.2 Path (Lenses)
- 2.1 LabelledGeneric
- Coproduct
- Validated
- Semigroup
- Monoid
- 特性
- 基准测试
- 待办事项
- 贡献
- 灵感
- 维护者
示例
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上的这个问题)
Generic提供了一个方便的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
LabelledGeneric
除了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
特质的实现可能会冲突。这在未来可能会改变(也许如果我们转向纯过程宏的方式?) - 对于包含许多嵌套字段并且需要
transmogfiy
的类型,使用这种技术可能会增加编译时间。 - 如果你在
transform_from
认为转换不可能(例如字段缺失)时对编译时错误感到不满,那么在需要递归transmogrify
的类型时,这些错误会更糟糕。
有关泛型和字段的更多信息,请查阅相应的 Rust 文档。
路径
继承自 LabelledGeneric
的结构可以做一些事情,比如使用 Path
和其伴随特质 PathTraverser
进行泛型遍历。在某些圈子中,这种功能也被称为镜头。
基于 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()]);
Semigroup
可以组合的事物。
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));
Monoid
可以组合 并且 具有空/ID 值的事物。
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 没有对 Higher Kinded Types 的支持,我不确定这些是否可以实施。此外,Rustaceans 习惯于在集合上调用 iter()
以获取懒视图,使用 map
或 and_then
操作它们的元素,然后在最后执行 collect()
以保持效率。在这些结构中,这些可能在该上下文中的用途有限。
函子
单调子
应用
应用子
贡献
请接受!
以下内容被认为很重要,符合 Rust 和函数式编程的精神
- 安全性(类型和内存)
- 效率
- 正确性
灵感
Scalaz、Shapeless、Cats、Haskell,常见的嫌疑人 ;)
维护者
也称为您可以在 Gitter 上打扰/标记/@的人 :D
lib.rs
:
Frunk 法则
此库包含可用于测试在 Frunk 中声明的代数实现的法则
依赖关系
~0.9–1.5MB
~31K SLoC