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)

MIT/Apache

160KB
2.5K SLoC

crates.io docs.rs Maintenance

naan

美味简洁

naan 是 Rust 语言的函数式编程前言,它是

  • 简单
  • 有用
  • stdalloc 可选
  • 快速 - 仅使用具体类型(无 dyn 亲和调度)意味着几乎零性能成本

目录

高阶类型

顶部 · 下一页 - 柯里化

HKT - 它是什么

顶部 · 上一页 - HKTs

在谈论类型时,区分具体类型(如 u8Vec<u8>Result<File, io::Error>)和未提供参数的泛型类型可能很有用。(如 VecOptionResult

例如,Vec 是一个一参数(一元)类型函数,而 Vec<u8> 是一个具体类型。

类型类别指的是一个类型有多少(如果有)参数。

HKT - 为什么它有用

顶部 · 向上 - HKT 在纯Rust中,Result::mapOption::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 引入了一些新的函数特性,这些特性增加了柯里化和函数组合的便利性;F1F2F3。这些特性通过允许柯里化和函数组合扩展了内置函数特性 FnFnOnce

(注意,每个阶数都有一个“可多次调用的”版本和一个“至少可调用一次”的版本。后者的特性以 Once 后缀表示)

F2F2Once 定义

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

类型类

顶部 · 上一页 - 函数组合

编程中最强大且实用的类型之一,被锁定在一个许多语言在高阶类型中不实施的功能之后。

mapunwrap_orand_then 这样的工具在日常 Rust 编程中非常有用,使我们能够方便地跳过大量手动编写的控制流。

比较 and_thenmap 与它们的反编译等效物

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

🔎 AltSemigroup 相同,但实现者是泛型的。

🔎 altResult::orOption::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)

PlusAlt 的基础上增加了“恒等”或“空”值,当与另一个值进行 alt 操作时不会进行任何操作。

🔎 PlusMonoid 相同,但实现者是泛型的。

示例

  • 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::mapOption::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,用来表示折叠函数只能调用一次。

折叠可以被看作是一系列步骤

  1. 给定一些可折叠的类型 F<T>,你想要得到一个 R
    • 我有一个 Vec<Option<u32>>,我想对值为 Some 的 u32 进行求和,并丢弃 None
  2. 从一个初始的 R 类型值开始
    • 因为我想得到 u32 的总和,所以我从零开始。
  3. 编写一个函数,类型为 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)
  4. 这个函数将对 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-2.0 许可证的定义,应按照上述方式双许可,无需任何附加条款或条件。

依赖