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日

960过程宏 中排名

Download history 44454/week @ 2024-03-14 47177/week @ 2024-03-21 38036/week @ 2024-03-28 48159/week @ 2024-04-04 42265/week @ 2024-04-11 41446/week @ 2024-04-18 46786/week @ 2024-04-25 43966/week @ 2024-05-02 44113/week @ 2024-05-09 38191/week @ 2024-05-16 38627/week @ 2024-05-23 41681/week @ 2024-05-30 35670/week @ 2024-06-06 39390/week @ 2024-06-13 37965/week @ 2024-06-20 29272/week @ 2024-06-27

149,477 每月下载量
用于 3 个crate(2个直接使用)

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,那么它可以帮助。

什么是“变形”?在这个上下文中,这意味着以类型安全的方式递归地将类型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 特性的 impl 中可能会发生冲突。这在未来可能会改变,也可能不会(也许如果我们转向纯过程宏的方式?)
  • 对于包含许多多级嵌套字段并需要 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

Validated 是运行可能出错的一组操作(例如,返回 Result<T, E> 的函数)的方法,如果发生一个或多个错误,将所有错误一次性返回给您。如果一切顺利,您将获得一个包含所有结果的 HList

映射(以及与其他plain对象一起工作)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

可以组合并且有一个空/标识值的事物。

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. Functor
  2. Monad
  3. Apply
  4. Applicative

贡献

是的,请!

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

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

灵感

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

维护者

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

  1. lloydmeta
  2. Centril
  3. ExpHP

lib.rs:

Frunk Proc Macros

这个库包含frunk的过程宏

链接

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

依赖关系

~290–740KB
~18K SLoC