#frunk #functional-programming #derive

frunk_derives

frunk_derives 包含了 Frunk 中某些特质的自定义派生

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

Download history 46728/week @ 2024-03-14 49369/week @ 2024-03-21 40073/week @ 2024-03-28 50168/week @ 2024-04-04 44075/week @ 2024-04-11 43460/week @ 2024-04-18 49144/week @ 2024-04-25 46110/week @ 2024-05-02 45687/week @ 2024-05-09 39771/week @ 2024-05-16 40410/week @ 2024-05-23 43314/week @ 2024-05-30 37413/week @ 2024-06-06 41435/week @ 2024-06-13 39630/week @ 2024-06-20 31129/week @ 2024-06-27

每月下载量 157,005
258 包使用(21 个直接使用)

MIT 许可证

29KB
548

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 无法帮助您,因为它不处理递归,但 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以获取懒视图,使用mapand_then来操作它们的元素,并在最后执行一个collect来保持高效。在那种情况下,以下结构的实用性可能有限。

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

贡献

是的,请!

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

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

灵感

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

维护者

即那些你在Gitter上可以bug/tag/@的人:D

  1. lloydmeta
  2. Centril
  3. ExpHP

lib.rs:

Frunk Derives

这个库包含了Frunk中一些好用的自定义推导逻辑。

链接

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

依赖项

~0.5–1MB
~20K SLoC