33 个版本
0.1.32 | 2023 年 3 月 18 日 |
---|---|
0.1.31 | 2023 年 3 月 18 日 |
0.1.27 | 2023 年 1 月 18 日 |
0.1.22 | 2022 年 12 月 24 日 |
0.1.7 | 2022 年 11 月 28 日 |
#252 in Rust 模式
在 2 个crate中使用 (via toad)
160KB
2.5K SLoC
naan
美味简洁
naan 是 Rust 语言的函数式编程前言,它是
- 简单
- 有用
std
和alloc
可选- 快速 - 仅使用具体类型(无
dyn
亲和调度)意味着几乎零性能成本
目录
高阶类型
HKT - 它是什么
顶部 · 上一页 - HKTs
在谈论类型时,区分具体类型(如 u8
,Vec<u8>
,Result<File, io::Error>
)和未提供参数的泛型类型可能很有用。(如 Vec
,Option
,Result
)
例如,Vec
是一个一参数(一元)类型函数,而 Vec<u8>
是一个具体类型。
类型类别指的是一个类型有多少(如果有)参数。
HKT - 为什么它有用
顶部 · 向上 - HKT 在纯Rust中,Result::map
和 Option::map
的形状非常相似
impl<A, E> Result<A, E> {
fn map<B>(self, f: impl FnMut(A) -> B) -> Result<B, E>;
}
impl<A> Option<A> {
fn map<B>(self, f: impl FnMut(A) -> B) -> Option<B>;
}
将它们两者都实现为 Map
特性将是有用的(我们将在后面详细说明原因)
trait Map<A> {
fn map<B>(self: Self<A>, f: impl FnMut(A) -> B) -> Self<B>;
}
impl<A> Map<A> for Option<A> {
fn map<B>(self, f: impl FnMut(A) -> B) -> Option<B> {
Option::map(self, f)
}
}
但是,这段代码在Rust中是不合法的,因为 Self
需要泛型,而在纯Rust中 Self
必须是具体类型。
HKT - 如何实现
顶部 · 上一页 - HKTs
随着 泛型关联类型 的引入,我们可以编写一个可以有效地替换“泛型self”功能的特性。
现在我们实际上可以合法、稳定地编写上述特性
trait HKT {
type Of<A>;
}
struct OptionHKT;
impl HKT for OptionHKT {
type Of<A> = Option<A>;
}
trait Map<M, A>
where M: HKT<Of<A> = Self>
{
fn map<B, F>(self, f: F) -> M::Of<B>
where F: FnMut(A) -> B;
}
impl<A> Map<OptionHKT, A> for Option<A> {
fn map<B, F>(self, f: F) -> Option<B>
where F: FnMut(A) -> B
{
Option::map(self, f)
}
}
柯里化
顶部 · 上一节 - HKT · 下一节 - 函数组合
柯里化 - 它是什么
柯里化 是 naan
获得其名称的技术。函数柯里化是将接受多个参数的函数拆分为多个函数的策略。
示例
fn foo(String, usize) -> usize;
foo(format!("bar"), 12);
将柯里化为
fn foo(String) -> impl Fn(usize) -> usize;
foo(format!("bar"))(12);
柯里化 - 为什么它有用
柯里化允许我们提供函数的一些参数,并在稍后日期提供此部分应用函数的其余参数。
这使得我们可以使用函数来存储状态,并使用 Apply
将接受任意数量参数的函数提升为接受 Results。
示例:具有存储参数的可重用函数
use std::fs::File;
use naan::prelude::*;
fn copy_file_to_dir(dir: String, file: File) -> std::io::Result<()> {
// ...
# Ok(())
}
fn main() {
let dir = std::env::var("DEST_DIR").unwrap();
let copy = copy_file_to_dir.curry().call(dir);
File::open("a.txt").bind1(copy.clone())
.bind1(|_| File::open("b.txt"))
.bind1(copy.clone())
.bind1(|_| File::open("c.txt"))
.bind1(copy);
}
/*
equivalent to:
fn main() {
let dir = std::env::var("DEST_DIR").unwrap();
copy_file_to_dir(dir.clone(), File::open("a.txt")?)?;
copy_file_to_dir(dir.clone(), File::open("b.txt")?)?;
copy_file_to_dir(dir, File::open("c.txt")?)?;
}
*/
示例:提升函数以接受 Results(或 Options)
use std::fs::File;
use naan::prelude::*;
fn append_contents(from: File, to: File) -> std::io::Result<()> {
// ...
# Ok(())
}
fn main() -> std::io::Result<()> {
Ok(append_contents.curry()).apply1(File::open("from.txt"))
.apply1(File::open("to.txt"))
.flatten()
}
/*
equivalent to:
fn main() -> std::io::Result<()> {
let from = File::open("from.txt")?;
let to = File::open("to.txt")?;
append_contents(from, to)
}
*/
柯里化 - 如何实现
naan 引入了一些新的函数特性,这些特性增加了柯里化和函数组合的便利性;F1
、F2
和 F3
。这些特性通过允许柯里化和函数组合扩展了内置函数特性 Fn
和 FnOnce
。
(注意,每个阶数都有一个“可多次调用的”版本和一个“至少可调用一次”的版本。后者的特性以 Once
后缀表示)
F2
和 F2Once
定义
F2
和 F2Once
定义pub trait F2Once<A, B, C>: Sized {
/// The concrete type that `curry` returns.
type Curried;
/// Call the function
fn call1(self, a: A, b: B) -> C;
/// Curry this function, transforming it from
///
/// `fn(A, B) -> C`
/// to
/// `fn(A) -> fn(B) -> C`
fn curry(self) -> Self::Curried;
}
pub trait F2<A, B, C>: F2Once<A, B, C> {
/// Call the function with all arguments
fn call(&self, a: A, b: B) -> C;
}
impl<F, A, B, C> F2<A, B, C> for F where F: Fn(A, B) -> C { /* <snip> */ }
impl<F, A, B, C> F2Once<A, B, C> for F where F: FnOnce(A, B) -> C { /* <snip> */ }
函数组合
组合 - 它是什么
顶部 · 上一节 - 函数组合
函数组合是按顺序连锁函数的策略,通过自动将一个函数的输出传递到另一个函数的输入。
这项非常强大的技术使我们能够以流经管道的数据来简洁地表示程序,而不是时间序列语句的序列。
use naan::prelude::*;
struct Apple;
struct Orange;
struct Grape;
#[derive(Debug, PartialEq)]
struct Banana;
fn apple_to_orange(a: Apple) -> Orange {
Orange
}
fn orange_to_grape(o: Orange) -> Grape {
Grape
}
fn grape_to_banana(g: Grape) -> Banana {
Banana
}
fn main() {
let apple_to_banana = apple_to_orange.chain(orange_to_grape)
.chain(grape_to_banana);
assert_eq!(apple_to_banana.call(Apple), Banana)
}
类型类
顶部 · 上一页 - 函数组合
编程中最强大且实用的类型之一,被锁定在一个许多语言在高阶类型中不实施的功能之后。
如 map
、unwrap_or
和 and_then
这样的工具在日常 Rust 编程中非常有用,使我们能够方便地跳过大量手动编写的控制流。
比较 and_then
和 map
与它们的反编译等效物
and_then
和 map
与它们的反编译等效物use std::io;
fn network_fetch_name() -> io::Result<String> {
Ok("harry".into())
}
fn network_send_message(msg: String) -> io::Result<()> {
Ok(())
}
fn global_state_store_name(name: &str) -> io::Result<()> {
Ok(())
}
// Declarative
fn foo0() -> io::Result<()> {
network_fetch_name().and_then(|name| {
global_state_store_name(&name)?;
Ok(name)
})
.map(|name| format!("hello, {name}!"))
.and_then(network_send_message)
}
// Idiomatic
fn foo1() -> io::Result<()> {
let name = network_fetch_name()?;
global_state_store_name(&name)?;
network_send_message(format!("hello, {name}!"))
}
// Imperative
fn foo2() -> io::Result<()> {
let name = match network_fetch_name() {
| Ok(name) => name,
| Err(e) => return Err(e),
};
match global_state_store_name(&name) {
| Err(e) => return Err(e),
| _ => (),
};
network_send_message(format!("hello, {name}!"))
}
一些注意事项
- “惯用”实现最简洁且易于扫描
- 由于作用域共享,惯用和命令式实现更难重构;命令式语句依赖于前面的语句才有意义,而声明式表达式与状态或作用域几乎没有耦合。
这些类型类的价值主张在于,它们允许我们将类型如 Result、Option 和 Iterators 视为抽象的 容器。
我们不需要了解太多关于它们的内部结构,就可以有效地和高效地使用它们。
这个极其简单但强大的隐喻使我们能够用具有共享接口的数据结构来解决一些非常复杂的问题。
半群和幺半群
组合具体类型的两个值
Semigroup
是我们赋予支持两个值某种结合操作的类型(a.append(b)
)的名称。
🔎 结合性意味着 a.append( b.append(c) )
必须等于 a.append(b).append(c)
。
示例
- 整数加法
1 * (2 * 3) == (1 * 2) * 3
- 整数乘法
1 + (2 + 3) == (1 + 2) + 3
- 字符串连接
"a".追加("b".追加("c")) == "a".追加("b").追加("c") == "abc"
Vec<T>
连接vec![1].追加(vec![2].追加(vec![3])) == vec![1, 2, 3]
Option<T>
(只有当T
实现Semigroup
时)Some("a").追加(Some("b")) == Some("ab")
Result<T, _>
(只有当T
实现Semigroup
时)Ok("a").追加(Ok("b")) == Ok("ab")
Monoid
通过一个“恒等”或“空”值扩展 Semigroup
,当追加到另一个值时不会做任何事情。
示例
- 整数加法中的 0
0 + 1 == 1
- 整数乘法中的 1
1 * 2 == 2
- 空字符串
String::恒等() == ""
"".追加("a") == "a"
Vec<T>
Vec::<u32>::恒等() == vec![]
vec![].追加(vec![1, 2]) == vec![1, 2]
这些定义为
pub trait Semigroup {
// 🔎 Note that this can be **any** combination of 2 selves,
// not just concatenation.
//
// The only rule is that implementations have to be associative.
fn append(self, b: Self) -> Self;
}
pub trait Monoid: Semigroup {
fn identity() -> Self;
}
Alt 和 Plus
组合同类型两个值的泛型类型
Alt
是我们赋予支持相同类型两个值的结合操作的泛型类型的名称(a.alt(b)
)。
🔎 Alt
与 Semigroup
相同,但实现者是泛型的。
🔎 alt
与 Result::or
和 Option::or
相同。
示例
Vec<T>
vec![1].alt(vec![2]) == vec![1, 2]
Result<T,_>
Ok(1).alt(Err(_)) == Ok(1)
Option<T>
None.alt(Some(1)) == Some(1)
Plus
在 Alt
的基础上增加了“恒等”或“空”值,当与另一个值进行 alt
操作时不会进行任何操作。
🔎 Plus
与 Monoid
相同,但实现者是泛型的。
示例
Vec<T>
(Vec::empty() == vec![]
)Option<T>
(Option::empty() == None
)
这些定义为
// 🔎 `Self` must be generic over some type `A`.
pub trait Alt<F, A>
where Self: Functor<F, A>,
F: HKT1<T<A> = Self>
{
fn alt(self, b: Self) -> Self;
}
pub trait Plus<F, A>
where Self: Alt<F, A>,
F: HKT1<T<A> = Self>
{
fn empty() -> F::T<A>;
}
Functor
使用函数转换容器内的值
Functor
是我们给予允许从 A -> B
中取函数并且有效地“穿透”一个类型并将其应用于某些 F<A>
,从而产生 F<B>
(a.fmap(a_to_b)
) 的类型的名称。
🔎 这与 Result::map
和 Option::map
相同。
🔎 存在一个单独的特质 FunctorOnce
,它扩展了 Functor
以知道映射函数只调用一次。
Functor
的定义如下:
// 🔎 `Self` must be generic over some type `A`
pub trait Functor<F, A> where F: HKT1<T<A> = Self>
{
// 🔎 given a function `A -> B`,
// apply it to the values of type `A` in `Self<A>` (if any),
// yielding `Self<B>`
fn fmap<AB, B>(self, f: AB) -> F::T<B> where AB: F1<A, B>;
}
Bifunctor
具有两个泛型参数的映射类型
Bifunctor
是我们给予具有两个泛型参数的类型的名称,这两个参数都可以被 map
。
Bifunctor
需要以下内容:
bimap
- 给定一个函数
A -> C
和另一个B -> D
,将T<A, B>
转换为T<C, D>
。
- 给定一个函数
Bifunctor
提供 2 个方法:
lmap
(映射左类型)T<A, B> -> T<C, B>
rmap
(映射右类型)T<A, B> -> T<A, D>
🔎 存在一个单独的特质 BifunctorOnce
,它扩展了 Bifunctor
以知道映射函数只调用一次。
Bifunctor
的定义如下:
pub trait Bifunctor<F, A, B>
where F: HKT2<T<A, B> = Self>
{
/// 🔎 In Result, this combines `map` and `map_err` into one step.
fn bimap<A2, B2, FA, FB>(self, fa: FA, fb: FB) -> F::T<A2, B2>
where FA: F1<A, A2>,
FB: F1<B, B2>;
/// 🔎 In Result, this maps the "Ok" type and is equivalent to `map`.
fn lmap<A2, FA>(self, fa: FA) -> F::T<A2, B>
where Self: Sized,
FA: F1<A, A2>
{
self.bimap(fa, |b| b)
}
/// 🔎 In Result, this maps the "Error" type and is equivalent to `map_err`.
fn rmap<B2, FB>(self, fb: FB) -> F::T<A, B2>
where Self: Sized,
FB: F1<B, B2>
{
self.bimap(|a| a, fb)
}
}
Foldable
解包 & 转换整个数据结构
可折叠的类型可以被解包并收集到新的值中。折叠是一个强大而复杂的操作,因为它非常通用;如果某个类型是可折叠的,它可以被折叠成几乎所有东西。
🔎 存在一个名为 FoldableOnce
的特质,它扩展了 Foldable
,用来表示折叠函数只能调用一次。
折叠可以被看作是一系列步骤
- 给定一些可折叠的类型
F<T>
,你想要得到一个R
- 我有一个
Vec<Option<u32>>
,我想对值为 Some 的 u32 进行求和,并丢弃 None
- 我有一个
- 从一个初始的
R
类型值开始- 因为我想得到 u32 的总和,所以我从零开始。
- 编写一个函数,类型为
Fn(R, T) -> R
。这个函数将被与初始的R
值以及来自F<T>
的一个类型为T
的值一起调用。函数将被重复调用,直到F<T>
中没有更多的T
|sum_so_far,option_of_u32|sum_so_far+option_of_u32.unwrap_or(0)
- 这个函数将对
F<T>
中包含的每个T
进行调用,并将它们收集到您提供的初始值R
中。vec![Some(1), None, Some(2), Some(4)].fold(|sum, n| sum+n.unwrap_or(0)) == 7
示例
将结果转换为 Option
use naan::prelude::*;
fn passing() -> Result<u32, ()> {
Ok(0)
}
fn failing() -> Result<u32, ()> {
Err(())
}
assert_eq!(match passing() {
| Ok(t) => Some(t),
| _ => None,
},
Some(0));
assert_eq!(passing().fold1(|_, t| Some(t), None), Some(0));
assert_eq!(failing().fold1(|_, t| Some(t), None), None);
折叠 Vec
use naan::prelude::*;
assert_eq!(vec![1, 2, 3].foldl(|sum, n| sum + n, 0), 6);
assert_eq!(vec![2, 4, 6].foldl(|sum, n| sum * n, 1), 48);
assert_eq!(vec!["a", "b", "c"].foldl(|acc, cur| format!("{acc}{cur}"), String::from("")),
"abc");
Foldable
的定义如下
pub trait Foldable<F, A> where F: HKT1<T<A> = Self>
{
/// Fold the data structure from left -> right
fn foldl<B, BAB>(self, f: BAB, b: B) -> B
where BAB: F2<B, A, B>;
/// Fold the data structure from right -> left
fn foldr<B, ABB>(self, f: ABB, b: B) -> B
where ABB: F2<A, B, B>;
/// Fold the data structure from left -> right
fn foldl_ref<'a, B, BAB>(&'a self, f: BAB, b: B) -> B
where BAB: F2<B, &'a A, B>,
A: 'a;
/// Fold the data structure from right -> left
fn foldr_ref<'a, B, ABB>(&'a self, f: ABB, b: B) -> B
where ABB: F2<&'a A, B, B>,
A: 'a;
}
🔎 Foldable
提供了许多由上述必需方法派生出的额外方法。完整文档可以在这里找到:这里.
use naan::prelude::*;
fn is_odd(n: &usize) -> bool {
n % 2 == 1
}
fn is_even(n: &usize) -> bool {
n % 2 == 0
}
assert_eq!(Some("abc".to_string()).fold(), "abc".to_string());
assert_eq!(Option::<String>::None.fold(), "");
let abc = vec!["a", "b", "c"].fmap(String::from);
assert_eq!(abc.clone().fold(), "abc");
assert_eq!(abc.clone().intercalate(", ".into()), "a, b, c".to_string());
assert_eq!(vec![2usize, 4, 8].any(is_odd), false);
assert_eq!(vec![2usize, 4, 8].all(is_even), true);
延迟 I/O
许可证
许可协议为以下之一
- Apache License, Version 2.0, (LICENSE-APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT 或 https://opensource.org/licenses/MIT)
任选其一。
贡献
除非您明确声明,否则任何有意提交以包含在作品中的贡献,根据 Apache-2.0 许可证的定义,应按照上述方式双许可,无需任何附加条款或条件。