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 中使用

MIT 许可证

78KB
1.5K SLoC

Frunk Crates.io 持续集成 Gitter 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

目录

  1. HList
  2. 泛型
  3. Coproduct
  4. Validated
  5. Semigroup
  6. Monoid
  7. 特性
  8. 基准测试
  9. 待办事项
  10. 贡献
  11. 灵感
  12. 维护者

示例

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() 以获取懒视图,使用 mapand_then 操作它们的元素,然后在最后执行 collect() 以保持效率。在这些结构中,这些可能在该上下文中的用途有限。

  1. 函子
  2. 单调子
  3. 应用
  4. 应用子

贡献

请接受!

以下内容被认为很重要,符合 Rust 和函数式编程的精神

  • 安全性(类型和内存)
  • 效率
  • 正确性

灵感

Scalaz、Shapeless、Cats、Haskell,常见的嫌疑人 ;)

维护者

也称为您可以在 Gitter 上打扰/标记/@的人 :D

  1. lloydmeta
  2. Centril
  3. ExpHP

lib.rs:

Frunk 法则

此库包含可用于测试在 Frunk 中声明的代数实现的法则

依赖关系

~0.9–1.5MB
~31K SLoC