Rust是一种多范式编程语言,专注于安全性、速度和并发性。除了过程和面向对象的编程模式外,它还支持函数式编程模式。函数式编程(FP)是一种编程范式,其中通过应用和组合函数来构造程序。它强调纯函数、不变性和递归,而不是可变数据和迭代。
函数式编程的一些主要好处是:
1.易于推理和测试
2.没有副作用,函数是确定的
3.由于不可变性,并发性更容易
4.代码通常更简洁
5.可以利用像高阶函数这样强大的函数概念
在本文中,我们将通过示例来探索Rust中的核心函数式编程概念。
一等函数
当一种编程语言中的函数像其他变量一样被对待时,我们就说这种语言具有一等函数。
将函数存储在变量中
fn add(x: i32, y: i32) -> i32 {
x + y
}
let mut sum = add;
sum(1, 2); // Returns 3
这里我们将add函数存储在sum变量中,然后使用sum(1,2)调用它。
将函数作为参数传递给其他函数
我们可以将函数作为参数传递给其他函数:
fn call_with_two(func: fn(i32, i32) -> i32, x: i32) -> i32 {
func(x, 2)
}
call_with_two(add, 1); // Returns 3
这里我们定义了一个call_with_two函数,它接受另一个函数func和一个整数x作为参数。然后,它调用函数func(x, 2),并返回结果。我们通过传递add函数来调用它,它返回3。
从函数返回函数
我们也可以从其他函数返回函数:
fn make_adder(x: i32) -> fn(i32) -> i32 {
fn add(y: i32) -> i32 {
x + y
}
add
}
let add_10 = make_adder(10);
add_10(3); // Returns 13
这里make_adder返回一个函数。我们先将10传递给make_adder函数,将函数的返回值赋值给add_10,然后再调用参数为3的add_10函数。
闭包与迭代器
闭包是可以捕获其上下文环境中变量的匿名函数。闭包通常与迭代器一起使用,对集合的每个元素执行一些逻辑。
let vec = vec![1, 2, 3];
let doubled = vec.iter().map(|x| x * 2);
iter()方法返回vector对象上的迭代器。map()方法接受一个闭包作为参数,该闭包将在每个元素上调用。这里我们把每个元素乘以2。“|x| x * 2”语法定义了一个闭包。
可以将结果迭代器收集到一个新的vector对象中:
let doubled = vec.iter().map(|x| x * 2).collect();
// doubled is [2, 4, 6]
下面是一个使用带闭包的迭代器来过滤元素的例子:
let vec = vec![1, 2, 3, 4, 5];
let even = vec.iter().filter(|x| x % 2 == 0).collect();
// even is [2, 4]
filter()方法将调用每个元素的闭包,并且只保留闭包返回true的元素。
闭包可以捕获其上下文环境中的变量:
let x = 10;
let closure = |y| {
x + y
};
let answer = closure(2); // answer is 12
闭包使用外部作用域的x变量。
迭代器和闭包是Rust中必不可少的函数概念,它们支持很多有用的功能。
纯函数
纯函数对于函数式编程至关重要。纯函数有两个重要的性质:
1,它每次对相同的输入集合返回相同的输出。
2,它没有副作用——它不会改变任何外部状态。
让我们看一些Rust中纯函数和非纯函数的例子:
// 纯函数
fn add(x: i32, y: i32) -> i32 {
x + y
}
// 非纯函数 — 依赖于外部可变状态
static mut COUNTER: i32 = 0;
fn increment_counter() {
unsafe {
COUNTER += 1;
}
}
add()函数是纯函数,因为:
1.对于给定的x和y,它总是返回相同的结果。
2.它没有副作用 - 它不操纵任何外部状态。
increment_counter()函数是非纯函数,因为:
1.它操纵外部COUNTER变量,改变其状态。
要在Rust中编写纯函数,我们需要遵循一些准则:
1,永远不要改变外部状态(不使用mut)
2,永远不要读取可变静态变量(没有不安全块)
3,只将输入转换为输出
然而,只要我们改变局部声明的变量,仍然可以在纯函数中使用mut。例如:
fn add_one(mut x: i32) -> i32 {
x += 1;
x
}
这个函数是纯函数,因为尽管它改变了x,但x是一个局部声明的变量。突变不会影响函数外部的任何东西。
在Rust中编写纯函数的好处是:
1.很容易推理的
2.并发友好的
3.可缓存的
4.可测试的
递归函数
递归是函数调用自身的一种编程技术。递归函数一直调用自己,直到满足停止条件。
递归函数必须包含两个部分:
1,递归调用函数的停止条件
2,函数调用自身
下面是Rust中递归的一个基本例子:
fn count_down(n: i32) {
if n == 0 { // base case
println!("Done!");
} else { // recursive case
println!("{}", n);
count_down(n - 1); // function calls itself
}
}
fn main() {
count_down(5);
}
运行结果:
5
4
3
2
1
Done!
递归的一些好的用例是:
1.数学计算(如阶乘)
2.解决迷宫或寻路
3.树图遍历
4.分而治之算法
然而,递归应该小心使用。递归函数通常可以使用迭代循环重写,这样可以更好地执行并避免栈溢出错误。
高阶函数
高阶函数是对其他函数进行操作的函数,要么接受它们作为参数,要么返回它们。
什么是高阶函数?
高阶函数就是接受一个或多个函数作为参数或返回一个函数作为结果的函数。
例子如下:
fn apply<F>(f: F) -> F {
f
}
fn compose<A, B, C>(f: fn(A) -> B, g: fn(B) -> C) -> fn(A) -> C {
move |x| g(f(x))
}
fn twice<F>(f: F) -> F
where
F: FnOnce() -> ()
{
move || {
f();
f();
}
}
我们可以这样使用它们:
fn add_5(x: i32) -> i32 {
x + 5
}
fn multiply(x: i32, y: i32) -> i32 {
x * y
}
fn apply_example() {
let f = apply(add_5);
let result = f(10);
}
fn compose_example() {
let h = compose(add_5, multiply);
let result = h(2, 3);
}
fn twice_example() {
let f = twice(|| println!("Hello!"));
f();
}
我们可以使用高阶函数和迭代器来执行强大的操作。例如:
let nums = vec![1, 2, 3, 4, 5];
// Map
let doubles = nums.iter().map(|x| x * 2);
// [2, 4, 6, 8, 10]
// Filter
let evens = nums.iter().filter(|&x| x % 2 == 0);
// [2, 4]
// Fold
let sum = nums.iter().fold(0, |a, b| a + b);
// 15
总结
在本文中,我们探讨了Rust中函数式编程的基础知识。然而,函数式编程并不总是正确的解决方案。我们需要考虑性能、突变需求和状态更改。