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

Download history 46935/week @ 2024-03-14 49742/week @ 2024-03-21 40452/week @ 2024-03-28 50502/week @ 2024-04-04 44499/week @ 2024-04-11 43808/week @ 2024-04-18 49524/week @ 2024-04-25 46423/week @ 2024-05-02 46093/week @ 2024-05-09 40252/week @ 2024-05-16 40854/week @ 2024-05-23 43728/week @ 2024-05-30 37868/week @ 2024-06-06 41825/week @ 2024-06-13 39958/week @ 2024-06-20 31307/week @ 2024-06-27

每月下载量 158,429
260 crate 中使用(直接使用 4 个)

MIT 许可证

210KB
3.5K SLoC

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

目录

  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() 的错误会更糟,程度取决于您的类型是否需要递归 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() 以获得懒视图,使用 mapand_then 操作它们的元素,然后在最后执行 collect() 以保持高效。在这些后续结构中的有用性可能有限。

  1. 函子(Functor)
  2. 单调(Monad)
  3. 应用(Apply)
  4. 适用性(Applicative)

贡献

是的,请吧!

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

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

灵感

Scalaz、Shapeless、Cats、Haskell,以及其他可疑者 ;)

维护者

也就是你可以在 Gitter 上打扰/标记/@ 的人 :D

  1. lloydmeta
  2. Centril
  3. ExpHP

lib.rs:

Frunk Proc Macro 内部

此库包含 frunk 使用的过程宏的通用逻辑

链接

  1. Github 上的源代码
  2. Crates.io 页面

依赖项

~290–750KB
~18K SLoC