#frunk #macro #proc-macro #internal

macro frunk_proc_macros_impl

Frunk的过程宏内部实现

7个版本

0.1.1 2022年11月3日
0.1.0 2021年6月25日
0.0.5 2021年4月16日
0.0.4 2019年12月21日
0.0.2 2019年2月9日

#11 in #frunk

Download history 2489/week @ 2024-03-14 3509/week @ 2024-03-21 2787/week @ 2024-03-28 2558/week @ 2024-04-04 2542/week @ 2024-04-11 2802/week @ 2024-04-18 2699/week @ 2024-04-25 2432/week @ 2024-05-02 2716/week @ 2024-05-09 2816/week @ 2024-05-16 2390/week @ 2024-05-23 2195/week @ 2024-05-30 1773/week @ 2024-06-06 1883/week @ 2024-06-13 1925/week @ 2024-06-20 1362/week @ 2024-06-27

7,222 每月下载量

MIT 协议

215KB
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. 验证
  5. 半群
  6. 幺半群
  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])

同样,您可以通过sculpt()方法重新塑造或雕刻Hlist,这个方法允许您根据类型重新组织和/或剔除元素。与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() "推断索引";这是因为 implTransmogrifier 特性可能发生冲突。这可能在将来发生变化(也许如果我们转向纯过程宏方式?)
  • 对于包含许多多级嵌套字段且需要 transmogfiy() 的类型,使用此技术可能会增加编译时间。
  • 如果你对 transform_from 时的编译时错误感到沮丧,因为转换被认为是不可能的(例如缺少字段),那么当需要递归 transmogrify() 时,错误会更糟。

有关泛型和字段如何工作的更多信息,请参阅各自的 Rustdocs

路径

使用 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 是运行多个可能出错的操作(例如,返回 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()]);

半群

可以组合的事物。

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));

幺半群

可以组合并且具有空/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以获取懒视图,使用mapand_then操作它们的元素,然后最终执行一个collect来保持效率。在这些后续结构中的实用性可能有限。

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

贡献

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

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

灵感来源

Scalaz,Shapeless,Cats,Haskell,通常是嫌疑人;)

维护者

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

  1. lloydmeta
  2. Centril
  3. ExpHP

依赖关系

~300–760KB
~18K SLoC