Skip to content
📖 Auto 编程语言(主书)本书目录

闭包与迭代器 ​

在第 3 章中,我们介绍了函数作为 Auto 的基本构建块之一。函数让你可以为一段可 复用的逻辑命名,然后通过名称调用它。但有时你需要更灵活的东西——一个可以存储 在变量中、作为参数传递或从另一个函数返回的函数。这就是_闭包_提供的功能。

一旦有了闭包,一种强大的模式就出现了:通过小而可组合的函数来转换、过滤和归约 序列中的元素。这就是_迭代器_提供的功能。

闭包和迭代器共同构成了 Auto 函数式编程的基石。本章将介绍这两者。

闭包:函数作为值 ​

闭包_是一个匿名函数——一个没有名字的函数。你可以将它存储在变量中,作为参数 传递给另一个函数,或从一个函数中返回它。"闭包"这个词来源于这样一个事实:这 种函数可以_关闭(捕获)其定义所在作用域中的变量。

闭包语法 ​

Auto 使用 fn(params) returnType { body } 语法来表示闭包。这与命名函数使用 的 fn 关键字相同,但没有名称:

闭包作为值:将函数传递给 `apply`

让我们分解关键思想:

  1. fn apply(fn fn(int)int, x int) int —— 参数类型 fn(int)int 表示"一个 接受 int 并返回 int 的函数"。参数名前的 fn 关键字是类型语法的一部分 ——它告诉编译器这个参数是一个函数,而不是普通值。在函数体内,fn(x) 用参数 x 调用闭包。

  2. var double = fn(x int) int { x * 2 } —— 创建一个闭包并将其存储在变量 double 中。闭包接受一个 int 参数 x,返回 x * 2。变量 double 现在 持有一个函数值,你可以像 double(5) 这样调用它。

  3. apply(double, 5) —— 将 double 闭包作为第一个参数传递给 apply。在 apply 内部,闭包被调用并传入 5,产生 10。同样的模式适用于 add_one ,它对其参数加 1。

这种模式——将函数传递给另一个函数——被称为_高阶函数_。apply 函数是高阶函数, 因为它接受一个函数作为参数。高阶函数是函数式编程的基础。

在 Rust 中,闭包使用 |params| body 语法。在 Python 中,它们使用 lambda。 在 C 中,它们变成函数指针(简单情况)或包含函数指针和捕获状态的结构体。在 TypeScript 中,它们是箭头函数。

捕获变量 ​

闭包真正的威力在于_捕获_——在闭包体内使用周围作用域中的变量的能力。当闭包捕获 一个变量时,即使周围作用域已经退出,它仍然"记住"该变量。

从环境中捕获变量的闭包

以下是发生了什么:

  1. fn make_multiplier(factor int) fn(int)int —— 此函数接受一个 int 参数 factor 并返回一个 fn(int)int 类型的闭包。返回类型 fn(int)int 告诉编译 器:"此函数返回一个接受一个 int 并返回一个 int 的函数。"

  2. fn(n int) int { n * factor } —— 这是被返回的闭包。它从周围作用域( make_multiplier 函数的参数)捕获了 factor。即使 make_multiplier 返回 之后,闭包仍然可以访问 factor。

  3. var triple = make_multiplier(3) —— 以 factor = 3 调用 make_multiplier。返回的闭包"记住" factor 是 3。每次调用 triple(n) 都会计算 n * 3。

  4. triple(5) 产生 15,triple(10) 产生 30。两次调用都使用捕获的 值 factor = 3。

这种模式有时被称为_工厂函数_——一个创建并返回其他函数的函数,每个返回的函数都 带有不同的捕获状态。你可以调用 make_multiplier(2) 获得一个加倍函数,或调用 make_multiplier(10) 获得一个乘以十的函数。每个返回的闭包都是独立的。

在 Rust 中,move 关键字将捕获变量的所有权转移到闭包中。在 Python 中,内部 函数自然地捕获封闭作用域中的变量。在 C 中,闭包表示为一个结构体,包含捕获的 状态(factor)和函数指针。在 TypeScript 中,内部箭头函数通过词法作用域捕获 外部函数的变量。

迭代器:惰性序列 ​

_迭代器_是一个逐个产生值序列的对象。你已经在第 3 章的 for 循环和第 2 章的 范围中隐式地使用了迭代器。现在让我们看看它们在底层是如何工作的。

.iter() 和 .next() ​

Auto 中的每个集合都可以通过 .iter() 方法产生一个迭代器。迭代器有一个 .next() 方法,每次调用返回下一个元素。当没有更多元素时,.next() 返回 None。

创建迭代器并手动调用 `.next()`

关键观察:

  1. var iter = numbers.iter() —— 为数组 [1, 2, 3, 4, 5] 创建一个迭代器。 迭代器不复制数据——它持有对原始数组的引用(或指针)和一个位置计数器。

  2. iter.next() —— 第一次调用返回 Some(1),第二次返回 Some(2),依此类 推。最后一个元素之后,返回 None。每次调用将内部位置前进一位。

  3. 迭代器是_惰性的_ —— 它不会预先处理所有元素。只有当你请求时才产生下一个 值。这种惰性使得链接迭代器方法变得高效。

你自第 3 章以来一直使用的 for 循环实际上是 .iter() 和 .next() 的语法糖。 当你写 for n in numbers { ... } 时,Auto 内部创建一个迭代器并循环调用 .next() 直到得到 None。

在 Rust 中,.iter() 返回 Option<&T>,所以 println!("{:?}", iter.next()) 会打印 Some(1)、Some(2) 等。在 Python 中,next() 在最后一个元素之后抛出 StopIteration(或者你可以使用自动处理此情况的 for 循环)。在 C 中,迭代器 实现为一个带有指针和位置的结构体,耗尽时返回 NULL。在 TypeScript 中,生成器 提供相同的接口,.next() 返回 { value, done }。

转换:.map() ​

.map() 方法通过对序列中的每个元素应用闭包来转换它们。它产生一个包含转换后 值的新序列。

使用 `.map()` 将每个元素翻倍

以下是逐步发生的过程:

  1. numbers.map(fn(x) int { x * 2 }) —— .map() 方法接受一个闭包,并将 其应用于数组的每个元素。闭包 fn(x) int { x * 2 } 接受一个参数 x(从数组 元素类型推断为 int)并返回 x * 2。结果是一个新数组 [2, 4, 6, 8, 10]。

  2. for n in doubled —— 遍历结果数组并打印每个元素。

注意闭包参数 x 不需要显式的类型标注。Auto 从数组元素类型推断它。你可以写 fn(x int) int { x * 2 } 加上显式类型,但这不是必需的。这与 Rust 和 TypeScript 中闭包的类型推断原则相同。

在 Python 中,.map() 转译为列表推导式,这是 Python 的惯用等价写法。在 C 中, .map() 变成一个简单的 for 循环来填充新数组。在 TypeScript 中,.map() 是 原生的 Array 方法。

过滤:.filter() ​

.filter() 方法只保留满足谓词的元素——谓词是一个返回 bool 的闭包。谓词返回 false 的元素将被丢弃。

使用 `.filter()` 只保留偶数

关键思想:

  1. numbers.filter(fn(x int) bool { x % 2 == 0 }) —— .filter() 方法接受 一个谓词闭包。对于每个元素,它调用谓词。如果谓词返回 true,保留该元素。 如果返回 false,丢弃该元素。结果是一个新数组 [2, 4, 6, 8, 10]。

  2. 谓词闭包 —— fn(x int) bool { x % 2 == 0 } 接受一个 int 并返回一个 bool。表达式 x % 2 == 0 在 x 为偶数时为 true,在 x 为奇数时为 false。

  3. .filter() 不修改原始数组 —— 它产生一个仅包含通过过滤的元素的新数组。 原始的 numbers 数组保持不变。

在 Rust 中,.filter() 返回一个惰性迭代器适配器——它不会分配新的向量,直到 调用 .collect()。在 Python 中,等价写法是带有 if 子句的列表推导式。在 C 中,过滤器实现为一个循环,将匹配的元素复制到新数组中。在 TypeScript 中, .filter() 是原生的 Array 方法。

归约:.fold() ​

.fold() 方法通过反复应用一个将累加器与每个元素组合的闭包,将序列归约为单个 值。它接受两个参数:累加器的初始值,以及一个接受当前累加器和当前元素并返回新 累加器的闭包。

使用 `.fold()` 求和与求积

让我们分解一下:

  1. numbers.fold(0, fn(acc int, x int) int { acc + x }) —— 第一个参数 0 是累加器的初始值。闭包接受两个参数:acc(到目前为止的运行总和)和 x (当前元素)。它返回 acc + x,成为新的累加器。fold 按顺序处理每个元素:

    • 从 acc = 0 开始
    • acc = 0 + 1 = 1
    • acc = 1 + 2 = 3
    • acc = 3 + 3 = 6
    • acc = 6 + 4 = 10
    • acc = 10 + 5 = 15
    • 最终结果:15
  2. numbers.fold(1, fn(acc int, x int) int { acc * x }) —— 相同模式,但从 1 开始并相乘:1 * 1 * 2 * 3 * 4 * 5 = 120。

  3. 初始值很重要 —— 求和时,初始值是 0(加法单位元)。求积时,是 1(乘法 单位元)。选择错误的初始值会产生错误的结果。

.fold() 在其他语言中有时被称为 reduce 或 accumulate。它是最通用的迭代器 方法——你可以用 .fold() 实现 .map() 和 .filter(),但那样可读性较差。

在 Python 中,.fold() 映射为 functools.reduce。在 C 中,它变成带有累加器 变量的 for 循环。在 TypeScript 中,.fold() 映射为 Array.reduce。

链式调用 ​

迭代器的真正威力在_链接_多个方法时显现。每个方法返回一个新的迭代器(或集合) ,所以你可以将一个方法的输出管道化到下一个方法的输入。这创建了一个_管道_,将 计算描述为一系列转换。

将 `.filter()`、`.map()` 和 `.fold()` 链接为管道

以下是管道逐步的工作方式:

  1. 开始:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

  2. .filter(fn(x int) bool { x % 2 == 0 }) —— 只保留偶数: [2, 4, 6, 8, 10]

  3. .map(fn(x int) int { x * x }) —— 将每个数平方: [4, 16, 36, 64, 100]

  4. .fold(0, fn(acc int, x int) int { acc + x }) —— 求和: 4 + 16 + 36 + 64 + 100 = 220

  5. print(result) —— 输出 220。

管道从左到右自然地阅读:"取这些数字,过滤偶数,平方,然后求和。"每一步都是一 个小型、自包含的转换。将此与命令式的等价写法——带有可变累加器和条件逻辑的循环 ——相比,你可以看到为什么函数式风格在数据转换中通常更清晰。

在 Rust 中,.filter() 和 .map() 是惰性迭代器适配器。它们不会分配中间向量 。只有 .fold()(消耗性适配器)才会强制求值。这意味着 Rust 版本一次处理每个 元素通过整个管道,零中间分配。在 Python 中,列表推导式创建中间列表。在 C 中, 编译器可以将单个循环优化为高效的机器代码。在 TypeScript 中,.filter() 和 .map() 返回新数组,而 .reduce() 产生最终值。

总结 ​

本章介绍了 Auto 函数式编程的两大支柱:

概念语法用途
闭包fn(x int) int { x * 2 }匿名函数作为值
高阶函数fn apply(fn fn(int)int, ...)接受/返回函数的函数
捕获fn(n int) int { n * factor }闭包使用周围变量
迭代器.iter()创建序列生成器
下一个元素.next()从迭代器获取下一个值
转换.map(fn(x) int { ... })对每个元素应用函数
过滤.filter(fn(x int) bool { ... })保留匹配谓词的元素
归约.fold(init, fn(acc, x) int { ... })将序列折叠为单个值
链式调用.filter(...).map(...).fold(...)将转换组合为管道

核心要点:

  1. 闭包是函数作为值 —— 你可以将它们存储在变量中、作为参数传递、从其他函数 返回。它们捕获其封闭作用域中的变量。

  2. 高阶函数实现抽象 —— apply、make_multiplier 和迭代器方法都是高阶函数。 它们将_什么_(转换)与_如何_(迭代逻辑)分离。

  3. 迭代器是惰性的 —— .iter() 创建一个轻量级游标。.filter() 和 .map() 在消耗性适配器如 .fold() 强制求值之前什么都不做。这使得链接变得高效。

  4. 链接创建可读管道 —— .filter().map().fold() 读起来像一个句子:"过滤, 然后转换,然后归约。"每一步都是一个小型、可测试、可组合的单元。

  5. 相同的模式适用于所有目标平台 —— 闭包和迭代器是通用的编程概念。Auto 将 它们转译为各目标平台惯用的等价形式:Rust(闭包和 Iterator trait)、Python (lambda 和推导式)、C(函数指针和循环)、TypeScript(箭头函数和 Array 方法)。

闭包和迭代器在 Rust 中是零成本抽象——它们编译为与手写循环相同的机器代码。Auto 继承了这种性能模型。你获得了表达性强、可读性好的代码,而不会牺牲速度。

在下一章中,我们将探索 comptime —— Auto 的编译时元编程系统,它允许你在编译 时执行代码来生成类型、值甚至整个函数。

基于 MIT 许可发布。