frunk:基于 Rust 的函数式编程工具库项目

Funktional generic type-level programming in Rust: HList, Coproduct, Generic, LabelledGeneric, Validated, Monoid and friends.

分支13Tags34
文件最后提交记录最后更新时间
2 个月前
7 年前
2 个月前
2 个月前
3 年前
2 个月前
2 个月前
2 个月前
1 年前
2 个月前
3 年前
2 个月前
2 个月前
2 个月前
2 个月前
9 年前
3 年前
3 年前

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. 和型
  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])

同样地,您可以重新塑造或雕刻一个 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上的这个问题

泛型提供了一个方便的 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

标记泛型

除了 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 特质的 impl 将会冲突。这一点在未来可能会改变,也可能不会(也许如果我们转向使用纯过程宏的方式处理问题)?
  • 对于包含许多需要 transmogrify() 的深层嵌套字段的类型,使用此技术可能会增加你的编译时间。
  • 如果你在遇到 transform_from 的编译时错误(例如,字段缺失)时感到困惑,那么 transmogrify() 的错误会更糟糕,因为你的类型需要递归地使用 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()]);

半群

可以组合的事物。

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

半群

可进行组合且具有空值/单位元的元素。

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 上跟踪/标记/@ 的人:)

  1. lloydmeta
  2. Centril
  3. ExpHP

项目介绍

Rust 中的功能性泛型类型级别编程:HList、Coproduct、Generic、LabelledGeneric、Validated、Monoid 及其相关概念。【此简介由AI生成】

定制我的领域
131.46 K71访问 GitHub