适合所有人的 Rust 简单函数式编程技术

2025-06-08

适合所有人的 Rust 简单函数式编程技术

最初发表于deepu.tech

函数式编程 (FP) 被大肆炒作,很多酷炫的年轻人都在尝试,但它并非灵丹妙药。与其他编程范式/风格一样,函数式编程也有其优缺点,人们可能更喜欢其中一种范式。如果您是 Rust 开发者,并且想要尝试函数式编程,不用担心,您无需学习面向函数式编程的语言,例如 Haskell 或 Clojure(甚至 Scala 或 JavaScript,尽管它们并非纯粹的函数式编程语言),因为 Rust 已经涵盖了您的需求,而这篇文章正是为您准备的。

如果您正在寻找 Java、Golang 或 TypeScript 中的函数式编程,请查看本系列的其他帖子。

我不会深入探讨所有函数式编程的概念,而是会专注于 Rust 中符合函数式编程概念的操作。我也不会泛泛地讨论函数式编程的优缺点。

请注意,为了方便您阅读,这篇文章中的一些介绍与我在该系列的其他文章中重复。


什么是函数式编程?

根据维基百科,

函数式编程是一种编程范式——一种构建计算机程序结构和元素的风格——它将计算视为数学函数的评估,并避免改变状态和可变数据。

因此,在函数式编程中,有两个非常重要的规则

  • 无数据突变:这意味着数据对象在创建后不应更改。
  • 无隐式状态:应避免使用隐藏/隐式状态。在函数式编程中,状态不会被消除,而是会使其变得可见且显式。

这意味着:

  • 无副作用:函数或操作不应改变其功能范围之外的任何状态。也就是说,函数应该只向调用者返回一个值,并且不应影响任何外部状态。这意味着程序更容易理解。
  • 仅限纯函数:函数式代码是幂等的。函数应该仅根据传入的参数返回值,并且不应影响(副作用)或依赖于全局状态。这样的函数对于相同的参数始终产生相同的结果。

除此之外,下面还有一些可以在 Rust 中应用的函数式编程概念,我们将在下文中讨论这些概念。

使用函数式编程并不意味着全有或全无,你始终可以使用函数式编程概念来补充 Rust 中的面向对象或命令式概念。无论你使用哪种范式或语言,都可以尽可能地利用函数式编程的优势。这正是我们将要看到的。


Rust 中的函数式编程

Rust 主要面向过程式/命令式编程风格,但它也支持函数式和面向对象编程风格。而这正是我最喜欢的混合风格。那么,让我们看看如何利用 Rust 的语言特性,将上面的一些函数式编程概念应用到 Rust 中。

一等函数和高阶函数

一等函数(函数作为一等公民)意味着您可以将函数分配给变量,将函数作为参数传递给另一个函数或从另一个函数返回一个函数。Rust 中的函数比其他语言更复杂一些,它不像 Go 或 JavaScript 中那么简单。函数有多种类型,并且有两种不同的编写方式。第一种是不能记忆其外部上下文的函数,第二种是可以记忆其外部上下文的闭包。因此,在 Rust 中可以实现诸如柯里化和高阶函数之类的概念,但可能不像在其他语言中那样容易理解。此外,接受闭包的函数可以根据上下文接受指向函数的指针。在许多地方,Rust 函数和闭包可以互换。如果函数很简单,我们可以在不依赖闭包的情况下完成以下所有操作,那就更好了。但 Rust 选择了这些妥协,以获得更好的内存安全性和性能。

只有当一个函数接受一个或多个函数作为参数,或者返回另一个函数作为结果时,它才可以被看作是高阶函数。
在 Rust 中,使用闭包很容易实现这一点,虽然看起来可能有点冗长,但如果你熟悉 Rust,应该没问题。

fn main() {
    let list = vec![
        String::from("Orange"),
        String::from("Apple"),
        String::from("Banana"),
        String::from("Grape"),
    ];
    // we are passing the array and a closure as arguments to map_for_each method.
    let out = map_for_each(list, |it: &String| -> usize {
        return it.len();
    });

    println!("{:?}", out); // [6, 5, 6, 5]
}

// The higher-order-function takes an array and a closure as arguments
fn map_for_each(list: Vec<String>, fun: fn(&String) -> usize) -> Vec<usize> {
    let mut new_array: Vec<usize> = Vec::new();
    for it in list.iter() {
        // We are executing the closure passed
        new_array.push(fun(it));
    }
    return new_array;
}
Enter fullscreen mode Exit fullscreen mode

您还可以使用泛型编写更复杂的版本,例如下面这样

fn main() {
    let list = vec![2, 5, 8, 10];
    // we are passing the array and a closure as arguments to map_for_each method.
    let out = map_for_each(list, |it: &usize| -> usize {
        return it * it;
    });

    println!("{:?}", out); // [4, 25, 64, 100]
}

// The higher-order-function takes an array and a closure as arguments, but uses generic types
fn map_for_each<A, B>(list: Vec<A>, fun: fn(&A) -> B) -> Vec<B> {
    let mut new_array: Vec<B> = Vec::new();
    for it in list.iter() {
        // We are executing the closure passed
        new_array.push(fun(it));
    }
    return new_array;
}
Enter fullscreen mode Exit fullscreen mode

但是,我们也可以使用内置函数式方法(例如 map、fold(reduce) 等)来简化这一过程,这样就简洁得多。Rust 提供了许多实用的函数式方法来处理集合,例如mapfoldfor_eachfilter等等。

fn main() {
    let list = ["Orange", "Apple", "Banana", "Grape"];

    // we are passing a closure as arguments to the built-in map method.
    let out: Vec<usize> = list.iter().map(|x| x.len()).collect();

    println!("{:?}", out); // [6, 5, 6, 5]
}
Enter fullscreen mode Exit fullscreen mode

Rust 中的闭包可以记忆并修改其外部上下文,但由于 Rust 的所有权概念,你不能让多个闭包修改外部上下文中的相同变量。在 Rust 中也可以进行柯里化,但由于所有权和生命周期概念,它可能会显得略显冗长。

fn main() {
    // this is a higher-order-function that returns a closure
    fn add(x: usize) -> impl Fn(usize) -> usize {
        // A closure is returned here
        // variable x is obtained from the outer scope of this method and memorized in the closure by moving ownership
        return move |y| -> usize { x + y };
    };

    // we are currying the add method to create more variations
    let add10 = add(10);
    let add20 = add(20);
    let add30 = add(30);

    println!("{}", add10(5)); // 15
    println!("{}", add20(5)); // 25
    println!("{}", add30(5)); // 35
}
Enter fullscreen mode Exit fullscreen mode

纯函数

正如我们已经看到的,纯函数应该仅根据传递的参数返回值,并且不应影响或依赖于全局状态。在 Rust 中可以轻松做到这一点。

如下所示,这是一个纯函数。对于给定的输入,它总是返回相同的输出,并且其行为高度可预测。如果需要,我们可以安全地缓存该方法。

fn sum(a: usize, b: usize) -> usize {
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

但是由于 Rust 变量默认是不可变的,除非特别指定,否则函数无法改变传递给它的任何变量,也无法捕获其上下文中的任何变量。因此,如果我们尝试像下面这样影响外部状态,编译器会报错“无法捕获 fn 项中的动态环境”。

use std::collections::HashMap;

fn main() {
    let mut holder = HashMap::new();

    fn sum(a: usize, b: usize) -> usize {
        let c = a + b;
        holder.insert(String::from(format!("${a}+${b}", a = a, b = b)), c);
        return c;
    }
}
Enter fullscreen mode Exit fullscreen mode

在 Rust 中,为了捕获外部状态,我们必须使用闭包,因此我们可以将上述内容重写为

use std::collections::HashMap;

fn main() {
    let mut holder = HashMap::new();

    let sum = |a: usize, b: usize| -> usize {
        let c = a + b;
        holder.insert(String::from(format!("${a}+${b}", a = a, b = b)), c);
        return c;
    };

    println!("{}", sum(10, 20));
}
Enter fullscreen mode Exit fullscreen mode

但编译仍然会失败,并显示“无法借用sum可变函数,因为它未声明为可变函数”。因此,为了实现外部状态的改变,我们必须明确地将函数指定为可变函数,例如let mut sum = ...

因此,Rust 默认会帮助你保持函数的纯粹和简洁。当然,这并不意味着你可以避免不涉及变量突变的副作用,对于这些副作用,你必须自己处理。

递归

函数式编程更倾向于使用递归而不是循环。让我们看一个计算数字阶乘的例子。我做了一些基准测试,并将 ns/op 的结果以注释的形式添加到了行内。

在传统的迭代方法中:

fn main() {
     // Average  8.5858 ns/op
    fn factorial(mut num: usize) -> usize {
        let mut result = 1;
        while num > 0 {
            result *= num;
            num = num - 1;
        }
        return result;
    }

    println!("{}", factorial(20)); // 2432902008176640000
}
Enter fullscreen mode Exit fullscreen mode

可以使用递归来完成相同的操作,如下所示,这在函数式编程中很受欢迎 - 但递归并不总是解决方案,在某些情况下,我个人认为简单的循环更具可读性。

fn main() {
     // Average  8.6150 ns/op
    fn factorial(num: usize) -> usize {
        return match num {
            0 => 1,
            _ => num * factorial(num - 1),
        };
    }

    println!("{}", factorial(20)); // 2432902008176640000
}
Enter fullscreen mode Exit fullscreen mode

递归方法的缺点通常是它可能导致堆栈溢出错误,因为每个函数调用都需要作为堆栈帧保存。为了避免这种情况,建议使用尾递归,尤其是在递归执行次数过多的情况下。在尾递归中,递归调用是函数执行的最后一个操作,因此编译器无需保存函数堆栈帧。大多数编译器可以像优化迭代代码一样优化尾递归代码,从而避免性能损失。在 Rust 中,这通常不是问题,因为 Rust 具有零成本抽象,大多数情况下命令式代码和递归代码都会编译为同一种汇编代码。

Rust 也支持尾调用优化,但并非总是如此。同样的尾调用用例如下所示。

fn main() {
     // Average  8.6869 ns/op
    fn factorial(num: usize) -> usize {
        factorial_inner(1, num)
    }

    fn factorial_inner(acc: usize, val: usize) -> usize {
        return match val {
            1 => acc,
            _ => factorial_tail_inner(acc * val, val - 1),
        };
    }

    println!("{}", factorial(20)); // 2432902008176640000
}
Enter fullscreen mode Exit fullscreen mode

对于阶乘,还有一种更好的方法,使用迭代器,其性能最佳

fn main() {
     // Average 6.6387 ns/op
    fn factorial(num: usize) -> usize {
        (1..num).fold(1, |n1, n2| n1 * n2)
    }

    println!("{}", factorial(20)); // 2432902008176640000
}
Enter fullscreen mode Exit fullscreen mode

在编写 Rust 代码时考虑使用递归以实现可读性和不变性,并且由于 Rust 的零成本抽象,我们不必担心性能。

惰性求值

惰性求值或非严格求值是指将表达式的求值延迟到需要时才进行的过程。通常,Rust 会进行严格/急切求值。我们可以利用高阶函数、闭包和记忆技术来实现惰性求值。

以这个 Rust 急切地评估一切的例子来说。

fn main() {
    fn add(x: usize) -> usize {
        println!("executing add"); // this is printed since the functions are evaluated first
        return x + x;
    }

    fn multiply(x: usize) -> usize {
        println!("executing multiply"); // this is printed since the functions are evaluated first
        return x * x;
    }

    fn add_or_multiply(add: bool, on_add: usize, on_multiply: usize) -> usize {
        if add {
            on_add
        } else {
            on_multiply
        }
    }
    println!("{}", add_or_multiply(true, add(4), multiply(4))); // 8
    println!("{}", add_or_multiply(false, add(4), multiply(4))); // 16
}
Enter fullscreen mode Exit fullscreen mode

这将产生以下输出,我们可以看到这两个函数始终执行

executing add
executing multiply
8
executing add
executing multiply
16
Enter fullscreen mode Exit fullscreen mode

我们可以使用高阶函数将其重写为惰性求值版本

fn main() {
    fn add(x: usize) -> usize {
        println!("executing add"); // this is printed since the functions are evaluated first
        return x + x;
    }

    fn multiply(x: usize) -> usize {
        println!("executing multiply"); // this is printed since the functions are evaluated first
        return x * x;
    }

    type FnType = fn(t: usize) -> usize;

    // This is now a higher-order-function hence evaluation of the functions are delayed in if-else
    fn add_or_multiply(add: bool, on_add: FnType, on_multiply: FnType, t: usize) -> usize {
        if add {
            on_add(t)
        } else {
            on_multiply(t)
        }
    }
    println!("{}", add_or_multiply(true, add, multiply, 4)); // 8
    println!("{}", add_or_multiply(false, add, multiply, 4)); // 16
}
Enter fullscreen mode Exit fullscreen mode

输出如下,我们可以看到只执行了必需的函数

executing add
8
executing multiply
16
Enter fullscreen mode Exit fullscreen mode

您还可以使用记忆/缓存技术来避免在纯函数和引用透明函数中进行不必要的计算,如下所示

use std::collections::HashMap;

fn main() {
    let mut cached_added = HashMap::new();

    let mut add = |x: usize| -> usize {
        return match cached_added.get(&x) {
            Some(&val) => val,
            _ => {
                println!("{}", "executing add");
                let out = x + x;
                cached_added.insert(x, out);
                out
            }
        };
    };

    let mut cached_multiplied = HashMap::new();

    let mut multiply = |x: usize| -> usize {
        return match cached_multiplied.get(&x) {
            Some(&val) => val,
            _ => {
                println!("executing multiply");
                let out = x * x;
                cached_multiplied.insert(x, out);
                out
            }
        };
    };

    fn add_or_multiply(add: bool, on_add: usize, on_multiply: usize) -> usize {
        if add {
            on_add
        } else {
            on_multiply
        }
    }

    println!("{}", add_or_multiply(true, add(4), multiply(4))); // 8
    println!("{}", add_or_multiply(false, add(4), multiply(4))); // 16
}

Enter fullscreen mode Exit fullscreen mode

这将输出以下内容,我们可以看到对于相同的值,函数仅执行了一次。

executing add
executing multiply
8
16
Enter fullscreen mode Exit fullscreen mode

这些代码可能看起来不太优雅,尤其是对于经验丰富的 Rust 程序员来说。幸运的是,Rust 提供的大多数函数式 API(例如迭代器)都支持惰性求值,并且有一些库(例如rust-lazyThunk)可以用来实现函数惰性求值。此外,Rust 还提供了一些高级类型来实现惰性求值。

有时,在 Rust 中进行惰性求值可能不值得增加代码复杂性,但如果所讨论的函数在处理方面很繁重,那么对它们进行惰性求值是值得的。

类型系统

Rust 是一门强大的静态类型语言,具有出色的类型推断能力。此外,它还拥有诸如类型别名等高级概念。

引用透明度

来自维基百科:

函数式程序没有赋值语句,也就是说,函数式程序中变量的值一旦定义就不会改变。这消除了任何副作用的可能性,因为任何变量都可以在执行的任何时刻被替换为其实际值。因此,函数式程序是引用透明的。

Rust 有很好的方法来确保引用透明性,Rust 中的变量默认是不可变的,甚至引用传递默认也是不可变的。因此,你必须显式地将变量或引用标记为可变才能做到这一点。所以在 Rust 中,避免变量或引用的改变相当容易。

例如,下面将产生一个错误

fn main() {
    let list = ["Apple", "Orange", "Banana", "Grape"];

    list = ["John", "Raju", "Sabi", "Vicky"];
}
Enter fullscreen mode Exit fullscreen mode

以下所有内容也同样如此

fn main() {
    let list = vec![
        String::from("Orange"),
        String::from("Apple"),
        String::from("Banana"),
        String::from("Grape"),
    ];

    list.push(String::from("Strawberry")); // This will fail as the reference is immutable

    fn mutating_fn(val: String) {
        val.push('!'); // this will fail unless the argument is marked mutable reference or value passed is marked mutable reference
    }

    mutating_fn(String::from("Strawberry")); // this will fail if the reference is not passed as mutable
}
Enter fullscreen mode Exit fullscreen mode

为了编译这些,我们必须用mut关键词来填充它

fn main() {
    let mut list = vec![
        String::from("Orange"),
        String::from("Apple"),
        String::from("Banana"),
        String::from("Grape"),
    ];

    list.push(String::from("Strawberry")); // This will work as the reference is mutable

    fn mutating_fn(val: &mut String) {
        val.push('!'); // this will work as the argument is marked as a mutable reference
    }

    mutating_fn(&mut String::from("Strawberry")); // this will work as the reference is passed as mutable
}

Enter fullscreen mode Exit fullscreen mode

当涉及到数据变异时,Rust 中还有更高级的概念,这些概念使得编写不可变代码变得更加容易。

数据结构

使用函数式编程技术时,建议使用诸如 Stacks、Maps 和 Queues 之类的数据类型,因为它们也具有函数式实现。因此,在函数式编程中,Hashmap 作为数据存储比数组或哈希集更佳。Rust 提供了此类数据类型,因此符合关于数据结构的函数式规范。


结论

对于那些尝试在 Rust 中应用一些函数式编程技术的人来说,这只是一个介绍。Rust 还有很多其他用途。正如我之前所说,函数式编程并非万能的灵丹妙药,但它提供了许多实用的技术,使代码更易于理解、维护和测试。它可以与命令式和面向对象的编程风格完美共存。事实上,我们都应该充分利用各种方法来解决手头的问题,而不是过于执着于单一的方法论。


希望本文对您有所帮助。如果您有任何疑问,或者觉得我遗漏了什么,欢迎留言。

如果您喜欢这篇文章,请点赞或留言。

您可以在TwitterLinkedIn上关注我。

鏂囩珷鏉ユ簮锛�https://dev.to/deepu105/easy-function-programming-techniques-in-rust-for-everyone-nae
PREV
正则表达式的 4 个实际用例 GenAI LIVE! | 2025 年 6 月 4 日
NEXT
管理多个 Git 配置文件