35 个版本
0.4.2 | 2023年6月17日 |
---|---|
0.4.1 | 2022年11月3日 |
0.4.0 | 2021年6月25日 |
0.3.1 | 2019年12月21日 |
0.0.12 | 2017年3月22日 |
在 过程宏 中排名第 209
每月下载量 157,005
被 258 个 包使用(21 个直接使用)
29KB
548 行
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
无法帮助您,因为它不处理递归,但 Transmogrifier
可以帮助您在两者都是 LabelledGeneric
通过 transmogrify()
从一个转换为另一个。
什么是“转换”?在这个上下文中,这意味着递归地将类型 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
进行泛型遍历。在某些领域,此功能也称为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()]);
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不支持高阶类型,我不确定这些是否可以实施。此外,Rustaceans习惯于在集合上调用iter
以获取懒视图,使用map
或and_then
来操作它们的元素,并在最后执行一个collect
来保持高效。在那种情况下,以下结构的实用性可能有限。
函子
单子
应用
应用函子
贡献
是的,请!
以下内容被认为很重要,符合Rust和函数式编程的精神
- 安全性(类型和内存)
- 效率
- 正确性
灵感
Scalaz、Shapeless、Cats、Haskell,通常是嫌疑人;)
维护者
即那些你在Gitter上可以bug/tag/@的人:D
lib.rs
:
Frunk Derives
这个库包含了Frunk中一些好用的自定义推导逻辑。
链接
依赖项
~0.5–1MB
~20K SLoC