函数式编程

2024/04/16

闭包

闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值:

fn main() { let x = 1; let sum = |y| x + y; assert_eq!(3, sum(2)); }
rust

上面的代码展示了非常简单的闭包 sum,它拥有一个入参 y,同时捕获了作用域中的 x 的值,因此调用 sum(2) 意味着将 2(参数 y)跟 1(x)进行相加,最终返回它们的和:3。

Rust 闭包的形式定义:

|param1, param2,...| { 语句1; 语句2; 返回表达式 }
rust

如果只有一个返回表达式的话,定义可以简化为:

|param1| 返回表达式
rust

闭包的类型推导

Rust 是静态语言,因此所有的变量都具有类型,但是得益于编译器的强大类型推导能力,在很多时候我们并不需要显式地去声明类型,但是显然函数并不在此列,必须手动为函数的所有参数和返回值指定类型,原因在于函数往往会作为 API 提供给你的用户,因此你的用户必须在使用时知道传入参数的类型和返回值类型。

与函数相反,闭包并不会作为 API 对外提供,因此它可以享受编译器的类型推导能力,无需标注参数和返回值的类型。

为了增加代码可读性,有时候我们会显式地给类型进行标注,出于同样的目的,也可以给闭包标注类型:

let sum = |x: i32, y: i32| -> i32 { x + y }
rust

与之相比,不标注类型的闭包声明会更简洁些:let sum = |x, y| x + y,需要注意的是,针对 sum 闭包,如果你只进行了声明,但是没有使用,编译器会提示你为 x, y 添加类型标注,因为它缺乏必要的上下文:

let sum = |x, y| x + y; let v = sum(1, 2);
rust

这里我们使用了 sum,同时把 1 传给了 x,2 传给了 y,因此编译器才可以推导出 x,y 的类型为 i32。

当编译器推导出一种类型后,它就会一直使用该类型

let example_closure = |x| x; let s = example_closure(String::from("hello")); let n = example_closure(5);
rust

首先,在 s 中,编译器为 x 推导出类型 String,但是紧接着 n 试图用 5 这个整型去调用闭包,跟编译器之前推导的 String 类型不符,因此报错:

error[E0308]: mismatched types --> src/main.rs:5:29 | 5 | let n = example_closure(5); | ^ | | | expected struct `String`, found integer // 期待String类型,却发现一个整数 | help: try using a conversion method: `5.to_string()`
bash

结构体中的闭包

假设我们要实现一个简易缓存,功能是获取一个值,然后将其缓存起来,那么可以这样设计:

  • 一个闭包用于获取值
  • 一个变量,用于存储该值

可以使用结构体来代表缓存对象,最终设计如下:

struct Cacher<T> where T: Fn(u32) -> u32, { query: T, value: Option<u32>, }
rust

这里使用泛型而不是直接在 query 上声明闭包是因为:每一个闭包实例都有独属于自己的类型,即使于两个签名一模一样的闭包,它们的类型也是不同的。

而标准库提供的 Fn 系列特征,再结合特征约束,就能很好的解决了这个问题. T: Fn(u32) -> u32 意味着 query 的类型是 T,该类型必须实现了相应的闭包特征 Fn(u32) -> u32

需要注意的是,其实 Fn 特征不仅仅适用于闭包,还适用于函数,因此上面的 query 字段除了使用闭包作为值外,还能使用一个具名的函数来作为它的值

接着,为缓存实现方法:

impl<T> Cacher<T> where T: Fn(u32) -> u32, { fn new(query: T) -> Cacher<T> { Cacher { query, value: None, } } // 先查询缓存值 `self.value`,若不存在,则调用 `query` 加载 fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.query)(arg); self.value = Some(v); v } } } }
rust

捕获作用域的值

fn main() { let x = 4; let equal_to_x = |z| z == x; let y = 4; assert!(equal_to_x(y)); }
rust

上面代码中,x 并不是闭包 equal_to_x 的参数,但是它依然可以去使用 x,因为 equal_to_xx 的作用域范围内。

对于函数来说,就算定义在 main 函数体中,它也不能访问 x:

fn main() { let x = 4; fn equal_to_x(z: i32) -> bool { z == x } let y = 4; assert!(equal_to_x(y)); }
rust

报错如下:

error[E0434]: can't capture dynamic environment in a fn item // 在函数中无法捕获动态的环境 --> src/main.rs:5:14 | 5 | z == x | ^ | = help: use the `|| { ... }` closure form instead // 使用闭包替代
bash

当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。

三种闭包函数特征

闭包捕获变量有三种途径,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用,因此相应的 Fn 特征也有三种。

FnOnce

FnOnce,该类型的闭包会拿走被捕获变量的所有权。Once 顾名思义,说明该闭包只能运行一次:

fn fn_once<F>(func: F) where F: FnOnce(usize) -> bool, { println!("{}", func(3)); println!("{}", func(4)); } fn main() { let x = vec![1, 2, 3]; fn_once(|z|{z == x.len()}) }
rust

仅实现 FnOnce 特征的闭包在调用时会转移所有权,所以显然不能对已失去所有权的闭包变量进行二次调用:

error[E0382]: use of moved value: `func` --> src\main.rs:6:20 | 1 | fn fn_once<F>(func: F) | ---- move occurs because `func` has type `F`, which does not implement the `Copy` trait // 因为`func`的类型是没有实现`Copy`特性的 `F`,所以发生了所有权的转移 ... 5 | println!("{}", func(3)); | ------- `func` moved due to this call // 转移在这 6 | println!("{}", func(4)); | ^^^^ value used here after move // 转移后再次用 |
bash

实际上为闭包加上 Copy 特征就可以二次调用:

fn fn_once<F>(func: F) where F: FnOnce(usize) -> bool + Copy,// 改动在这里 { println!("{}", func(3)); println!("{}", func(4)); } fn main() { let x = vec![1, 2, 3]; fn_once(|z|{z == x.len()}) }
rust

上面代码中,func 的类型 F 实现了 Copy 特征,调用时使用的将是它的拷贝,所以并没有发生所有权的转移,可以正常运行。

如果你想强制闭包取得捕获变量的所有权,可以在参数列表前添加 move 关键字,这种用法通常用于闭包的生命周期大于捕获变量的生命周期时,例如将闭包返回或移入其他线程:

use std::thread; let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Here's a vector: {:?}", v); }); handle.join().unwrap();
rust

如果闭包捕获的参数实现了 Copy 特征,那么则会出现一点不一样的情况:

fn main() { let mut count = 0; let mut inc = move || { count += 1; println!("`count`: {}", count); }; inc(); let _reborrow = &count; inc(); // The closure no longer needs to borrow `&mut count`. Therefore, it is // possible to reborrow without an error let _count_reborrowed = &mut count; assert_eq!(count, 0); }
rust

输出:

`count`: 1 `count`: 2
bash

代码成功退出,说明了在闭包中对 count 进行了复制,并且和外面的 count 一点关联都没有了,最后可以发现我们的 count 仍然是 0,而闭包中的 count 可以正常递增。

FnMut

FnMut,它以可变借用的方式捕获了环境中的值,因此可以修改该值:

fn main() { let mut s = String::new(); // 注意这里需要加上 mut let mut update_string = |str| s.push_str(str); update_string("hello"); println!("{:?}",s); }
rust

Fn

Fn 特征,它以不可变借用的方式捕获环境中的值:

fn main() { let mut s = String::new(); let update_string = |str| s.push_str(str); exec(update_string); println!("{:?}",s); } fn exec<'a, F: Fn(&'a str)>(mut f: F) { f("hello") }
rust

运行后:

error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnMut` --> src/main.rs:4:26 // 期望闭包实现的是`Fn`特征,但是它只实现了`FnMut`特征 | 4 | let update_string = |str| s.push_str(str); | ^^^^^^-^^^^^^^^^^^^^^ | | | | | closure is `FnMut` because it mutates the variable `s` here | this closure implements `FnMut`, not `Fn` //闭包实现的是FnMut,而不是Fn 5 | 6 | exec(update_string); | ---- the requirement to implement `Fn` derives from here
rust

因为在闭包函数中我们会修改 s,因此 update_stringFnMut 类型的闭包函数。

如果不对 s 进行修改:

fn main() { let s = "hello, ".to_string(); let update_string = |str| println!("{},{}",s,str); exec(update_string); println!("{:?}",s); } fn exec<'a, F: Fn(String) -> ()>(f: F) { f("world".to_string()) }
rust

在这里,因为无需改变 s,因此闭包中只对 s 进行了不可变借用,那么在 exec 中,将其标记为 Fn 特征就完全正确。

三种 Fn 的关系

实际上,一个闭包并不仅仅实现某一种 Fn 特征,规则如下:

  • 所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
  • 没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征
  • 不需要对捕获变量进行改变的闭包自动实现了 Fn 特征
fn main() { let s = String::new(); let update_string = || println!("{}",s); exec(update_string); exec1(update_string); exec2(update_string); } fn exec<F: FnOnce()>(f: F) { f() } fn exec1<F: FnMut()>(mut f: F) { f() } fn exec2<F: Fn()>(f: F) { f() }
rust

虽然,闭包只是对 s 进行了不可变借用,实际上,它可以适用于任何一种 Fn 特征:三个 exec 函数说明了一切。

闭包作为函数返回值

fn factory() -> Fn(i32) -> i32 { let num = 5; |x| x + num } let f = factory(); let answer = f(1); assert_eq!(6, answer);
rust

上述代码,看着很正常,实际编译时会报错:

fn factory<T>() -> Fn(i32) -> i32 { | ^^^^^^^^^^^^^^ doesn't have a size known at compile-time // 该类型在编译器没有固定的大小 help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/main.rs:11:5: 11:21]`, which implements `Fn(i32) -> i32` | 8 | fn factory<T>() -> impl Fn(i32) -> i32 {
bash

Rust 要求函数的参数和返回类型,必须有固定的内存大小,例如 i32 就是 4 个字节,引用类型是 8 个字节,总之,绝大部分类型都有固定的大小,但是不包括特征,因为特征类似接口,对于编译器来说,无法知道它后面藏的真实类型是什么,因为也无法得知具体的大小。

因此在这里编译器也提示我们使用 impl Fn(i32) -> i32,但是这种方式只能返回一种类型:

fn factory(x:i32) -> impl Fn(i32) -> i32 { let num = 5; if x > 1{ move |x| x + num } else { move |x| x - num } }
rust

运行后编译器报错:

error[E0308]: `if` and `else` have incompatible types --> src/main.rs:15:9 | 12 | / if x > 1{ 13 | | move |x| x + num | | ---------------- expected because of this 14 | | } else { 15 | | move |x| x - num | | ^^^^^^^^^^^^^^^^ expected closure, found a different closure 16 | | } | |_____- `if` and `else` have incompatible types | = help: consider boxing your closure and/or using it as a trait object
bash

这时编译器又提示使用 Box

fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> { let num = 5; if x > 1{ Box::new(move |x| x + num) } else { Box::new(move |x| x - num) } }
rust

至此,闭包作为函数返回值就已完美解决。

迭代器

For 循环与迭代器

在 rust 中的一个 for 循环如下:

let arr = [1, 2, 3]; for v in arr { println!("{}",v); }
rust

Rust 没有使用索引,它把 arr 数组当成一个迭代器,直接去遍历其中的元素,从哪里开始,从哪里结束,都无需操心。

虽然数组可以遍历,但是它并不是一个迭代器!它能迭代的原因是因为它实现了 IntoIterator 特征,只要实现了这个特征的类型都可以被 for 遍历。

类似的还有:

for i in 1..10 { println!("{}", i); }
rust

直接对数值序列进行迭代,也是很常见的使用方式。

IntoIterator 特征拥有一个 into_iter 方法,因此我们还可以显式的把数组转换成迭代器:

let arr = [1, 2, 3]; for v in arr.into_iter() { println!("{}", v); }
rust

手动遍历

Iterator 特征部分结构如下:

pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // 省略其余有默认实现的方法 }
rust

其中的 next 方法,就是控制如何从集合中取值,最终返回值的类型是关联类型 Item

for 循环也正是不断调用迭代器上的 next 方法,来获取迭代器中的元素。

我们也可以手动调用:

fn main() { let arr = [1, 2, 3]; let mut arr_iter = arr.into_iter(); assert_eq!(arr_iter.next(), Some(1)); assert_eq!(arr_iter.next(), Some(2)); assert_eq!(arr_iter.next(), Some(3)); assert_eq!(arr_iter.next(), None); }
rust

通过调用其上的 next 方法,我们获取了 arr 中的元素,有两点需要注意:

  • next 方法返回的是 Option 类型,当有值时返回 Some(i32),无值时返回 None
  • 遍历是按照迭代器中元素的排列顺序依次进行的,因此我们严格按照数组中元素的顺序取出了 Some(1)Some(2)Some(3)
  • 手动迭代必须将迭代器声明为 mut 可变,因为调用 next 会改变迭代器其中的状态数据(当前遍历的位置等),而 for 循环去迭代则无需标注 mut,因为它会帮我们自动完成

模拟 for 循环:

let values = vec![1, 2, 3]; { let result = match IntoIterator::into_iter(values) { mut iter => loop { match iter.next() { Some(x) => { println!("{}", x); }, None => break, } }, }; result }
rust

IntoIterator::into_iter 是使用完全限定的方式去调用 into_iter 方法,这种调用方式跟 values.into_iter() 是等价的。

同时我们使用了 loop 循环配合 next 方法来遍历迭代器中的元素,当迭代器返回 None 时,跳出循环。

IntoIterator 特征

into_iter, iter, iter_mut

除了 into_iter 外,还有 iteriter_mut 另外两个方法:

  • into_iter 会夺走所有权
  • iter 是不可变借用
  • iter_mut 是可变借用

具体用例如下:

fn main() { let values = vec![1, 2, 3]; for v in values.into_iter() { println!("{}", v) } // 下面的代码将报错,因为 values 的所有权在上面 `for` 循环中已经被转移走 // println!("{:?}",values); let values = vec![1, 2, 3]; let _values_iter = values.iter(); // 不会报错,因为 values_iter 只是借用了 values 中的元素 println!("{:?}", values); let mut values = vec![1, 2, 3]; // 对 values 中的元素进行可变借用 let mut values_iter_mut = values.iter_mut(); // 取出第一个元素,并修改为0 if let Some(v) = values_iter_mut.next() { *v = 0; } // 输出[0, 2, 3] println!("{:?}", values); }
rust

消费者与适配器

消费者是迭代器上的方法,它会消费掉迭代器中的元素,然后返回其类型的值,这些消费者都有一个共同的特点:在它们的定义中,都依赖 next 方法来消费元素,因此这也是为什么迭代器要实现 Iterator 特征,而该特征必须要实现 next 方法的原因。

消费者适配器

只要迭代器上的某个方法 A 在其内部调用了 next 方法,那么 A 就被称为消费性适配器:因为 next 方法会消耗掉迭代器上的元素,所以方法 A 的调用也会消耗掉迭代器上的元素。

其中一个例子是 sum 方法,它会拿走迭代器的所有权,然后通过不断调用 next 方法对里面的元素进行求和:

fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); let total: i32 = v1_iter.sum(); assert_eq!(total, 6); // v1_iter 是借用了 v1,因此 v1 可以照常使用 println!("{:?}",v1); // 以下代码会报错,因为 `sum` 拿到了迭代器 `v1_iter` 的所有权 println!("{:?}",v1_iter); }
rust

运行后,只有最后一段代码报错:

error[E0382]: borrow of moved value: `v1_iter` --> src/main.rs:14:21 | 4 | let v1_iter = v1.iter(); | ------- move occurs because `v1_iter` has type `std::slice::Iter<'_, i32>`, which does not implement the `Copy` trait 5 | 6 | let total: i32 = v1_iter.sum(); | ----- `v1_iter` moved due to this method call ... 14 | println!("{:?}",v1_iter); | ^^^^^^^ value borrowed here after move | note: `std::iter::Iterator::sum` takes ownership of the receiver `self`, which moves `v1_iter`
bash

迭代器适配器

迭代器适配器会返回一个新的迭代器,与消费者适配器不同,迭代器适配器是惰性的,意味着你需要一个消费者适配器来收尾,最终将迭代器转换成一个具体的值:

let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]);
rust

这里的 map 方法是一个迭代者适配器,它是惰性的,不产生任何行为,因此我们还需要一个消费者适配器(collect)进行收尾。

上面代码中,使用了 collect 方法,该方法就是一个消费者适配器,使用它可以将一个迭代器中的元素收集到指定类型中,这里我们为 v2 标注了 Vec<_> 类型,就是为了告诉 collect:请把迭代器中的元素消费掉,然后把值收集成 Vec<_> 类型,至于为何使用 _,因为编译器会帮我们自动推导。

再来看看如何使用 collect 收集成 HashMap 集合:

use std::collections::HashMap; fn main() { let names = ["sunface", "sunfei"]; let ages = [18, 18]; let folks: HashMap<_, _> = names.into_iter().zip(ages.into_iter()).collect(); println!("{:?}",folks); }
rust

zip 是一个迭代器适配器,它的作用就是将两个迭代器的内容压缩到一起,形成 Iterator<Item=(ValueFromA, ValueFromB)> 这样的新的迭代器,在此处就是形如 [(name1, age1), (name2, age2)] 的迭代器。

然后再通过 collect 将新迭代器中(K, V) 形式的值收集成 HashMap<K, V>,同样的,这里必须显式声明类型,然后 HashMap 内部的 KV 类型可以交给编译器去推导,最终编译器会推导出 HashMap<&str, i32>

实现 Iterator 特征

首先创建一个计数器:

struct Counter { count: u32, } impl Counter { fn new() -> Counter { Counter { count: 0 } } }
rust

之后为其实现 Iterator 特征:

impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { if self.count < 5 { self.count += 1; Some(self.count) } else { None } } }
rust

首先,将该特征的关联类型设置为 u32,由于我们的计数器保存的 count 字段就是 u32 类型, 因此在 next 方法中,最后返回的是实际上是 Option<u32> 类型。

每次调用 next 方法,都会让计数器的值加一,然后返回最新的计数值,一旦计数大于 5,就返回 None

最后,使用我们新建的 Counter 进行迭代:

let mut counter = Counter::new(); assert_eq!(counter.next(), Some(1)); assert_eq!(counter.next(), Some(2)); assert_eq!(counter.next(), Some(3)); assert_eq!(counter.next(), Some(4)); assert_eq!(counter.next(), Some(5)); assert_eq!(counter.next(), None); let sum: u32 = Counter::new() .zip(Counter::new().skip(1)) .map(|(a, b)| a * b) .filter(|x| x % 3 == 0) .sum(); assert_eq!(18, sum);
rust