8个版本
0.1.2 | 2023年6月17日 |
---|---|
0.1.1 | 2022年11月3日 |
0.1.0 | 2021年6月25日 |
0.0.5 | 2021年4月16日 |
0.0.2 | 2019年2月9日 |
在 过程宏 中排名 55
每月下载量 158,429
在 260 个 crate 中使用(直接使用 4 个)
210KB
3.5K SLoC
Frunk
frunk frəNGk
- Rust中的函数式编程工具集。
- 一开始可能感觉有点奇特,但你会喜欢的。
- 来源于:funktional(德语)+ Rust → Frunk
总体思路是通过在Rust中提供FP工具来简化事物,允许进行类似这样的操作
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()
的错误会更糟,程度取决于您的类型是否需要递归transmogrify()
。
有关泛型和字段如何工作的更多信息,请查阅各自的 Rustdocs。
路径
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()
以保持高效。在这些后续结构中的有用性可能有限。
函子(Functor)
单调(Monad)
应用(Apply)
适用性(Applicative)
贡献
是的,请吧!
以下内容被认为是重要的,与 Rust 和函数式编程的精神相符
- 安全性(类型和内存)
- 效率
- 正确性
灵感
Scalaz、Shapeless、Cats、Haskell,以及其他可疑者 ;)
维护者
也就是你可以在 Gitter 上打扰/标记/@ 的人 :D
lib.rs
:
Frunk Proc Macro 内部
此库包含 frunk 使用的过程宏的通用逻辑
链接
依赖项
~290–750KB
~18K SLoC