闭包与迭代器
在第 3 章中,我们介绍了函数作为 Auto 的基本构建块之一。函数让你可以为一段可 复用的逻辑命名,然后通过名称调用它。但有时你需要更灵活的东西——一个可以存储 在变量中、作为参数传递或从另一个函数返回的函数。这就是_闭包_提供的功能。
一旦有了闭包,一种强大的模式就出现了:通过小而可组合的函数来转换、过滤和归约 序列中的元素。这就是_迭代器_提供的功能。
闭包和迭代器共同构成了 Auto 函数式编程的基石。本章将介绍这两者。
闭包:函数作为值
闭包_是一个匿名函数——一个没有名字的函数。你可以将它存储在变量中,作为参数 传递给另一个函数,或从一个函数中返回它。"闭包"这个词来源于这样一个事实:这 种函数可以_关闭(捕获)其定义所在作用域中的变量。
闭包语法
Auto 使用 fn(params) returnType { body } 语法来表示闭包。这与命名函数使用 的 fn 关键字相同,但没有名称:
让我们分解关键思想:
fn apply(fn fn(int)int, x int) int—— 参数类型fn(int)int表示"一个 接受int并返回int的函数"。参数名前的fn关键字是类型语法的一部分 ——它告诉编译器这个参数是一个函数,而不是普通值。在函数体内,fn(x)用参数x调用闭包。var double = fn(x int) int { x * 2 }—— 创建一个闭包并将其存储在变量double中。闭包接受一个int参数x,返回x * 2。变量double现在 持有一个函数值,你可以像double(5)这样调用它。apply(double, 5)—— 将double闭包作为第一个参数传递给apply。在apply内部,闭包被调用并传入5,产生10。同样的模式适用于add_one,它对其参数加 1。
这种模式——将函数传递给另一个函数——被称为_高阶函数_。apply 函数是高阶函数, 因为它接受一个函数作为参数。高阶函数是函数式编程的基础。
在 Rust 中,闭包使用 |params| body 语法。在 Python 中,它们使用 lambda。 在 C 中,它们变成函数指针(简单情况)或包含函数指针和捕获状态的结构体。在 TypeScript 中,它们是箭头函数。
捕获变量
闭包真正的威力在于_捕获_——在闭包体内使用周围作用域中的变量的能力。当闭包捕获 一个变量时,即使周围作用域已经退出,它仍然"记住"该变量。
以下是发生了什么:
fn make_multiplier(factor int) fn(int)int—— 此函数接受一个int参数factor并返回一个fn(int)int类型的闭包。返回类型fn(int)int告诉编译 器:"此函数返回一个接受一个int并返回一个int的函数。"fn(n int) int { n * factor }—— 这是被返回的闭包。它从周围作用域(make_multiplier函数的参数)捕获了factor。即使make_multiplier返回 之后,闭包仍然可以访问factor。var triple = make_multiplier(3)—— 以factor = 3调用make_multiplier。返回的闭包"记住"factor是3。每次调用triple(n)都会计算n * 3。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。
关键观察:
var iter = numbers.iter()—— 为数组[1, 2, 3, 4, 5]创建一个迭代器。 迭代器不复制数据——它持有对原始数组的引用(或指针)和一个位置计数器。iter.next()—— 第一次调用返回Some(1),第二次返回Some(2),依此类 推。最后一个元素之后,返回None。每次调用将内部位置前进一位。迭代器是_惰性的_ —— 它不会预先处理所有元素。只有当你请求时才产生下一个 值。这种惰性使得链接迭代器方法变得高效。
你自第 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() 方法通过对序列中的每个元素应用闭包来转换它们。它产生一个包含转换后 值的新序列。
以下是逐步发生的过程:
numbers.map(fn(x) int { x * 2 })——.map()方法接受一个闭包,并将 其应用于数组的每个元素。闭包fn(x) int { x * 2 }接受一个参数x(从数组 元素类型推断为int)并返回x * 2。结果是一个新数组[2, 4, 6, 8, 10]。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 的元素将被丢弃。
关键思想:
numbers.filter(fn(x int) bool { x % 2 == 0 })——.filter()方法接受 一个谓词闭包。对于每个元素,它调用谓词。如果谓词返回true,保留该元素。 如果返回false,丢弃该元素。结果是一个新数组[2, 4, 6, 8, 10]。谓词闭包 ——
fn(x int) bool { x % 2 == 0 }接受一个int并返回一个bool。表达式x % 2 == 0在x为偶数时为true,在x为奇数时为false。.filter()不修改原始数组 —— 它产生一个仅包含通过过滤的元素的新数组。 原始的numbers数组保持不变。
在 Rust 中,.filter() 返回一个惰性迭代器适配器——它不会分配新的向量,直到 调用 .collect()。在 Python 中,等价写法是带有 if 子句的列表推导式。在 C 中,过滤器实现为一个循环,将匹配的元素复制到新数组中。在 TypeScript 中, .filter() 是原生的 Array 方法。
归约:.fold()
.fold() 方法通过反复应用一个将累加器与每个元素组合的闭包,将序列归约为单个 值。它接受两个参数:累加器的初始值,以及一个接受当前累加器和当前元素并返回新 累加器的闭包。
让我们分解一下:
numbers.fold(0, fn(acc int, x int) int { acc + x })—— 第一个参数0是累加器的初始值。闭包接受两个参数:acc(到目前为止的运行总和)和x(当前元素)。它返回acc + x,成为新的累加器。fold 按顺序处理每个元素:- 从
acc = 0开始 acc = 0 + 1 = 1acc = 1 + 2 = 3acc = 3 + 3 = 6acc = 6 + 4 = 10acc = 10 + 5 = 15- 最终结果:
15
- 从
numbers.fold(1, fn(acc int, x int) int { acc * x })—— 相同模式,但从1开始并相乘:1 * 1 * 2 * 3 * 4 * 5 = 120。初始值很重要 —— 求和时,初始值是
0(加法单位元)。求积时,是1(乘法 单位元)。选择错误的初始值会产生错误的结果。
.fold() 在其他语言中有时被称为 reduce 或 accumulate。它是最通用的迭代器 方法——你可以用 .fold() 实现 .map() 和 .filter(),但那样可读性较差。
在 Python 中,.fold() 映射为 functools.reduce。在 C 中,它变成带有累加器 变量的 for 循环。在 TypeScript 中,.fold() 映射为 Array.reduce。
链式调用
迭代器的真正威力在_链接_多个方法时显现。每个方法返回一个新的迭代器(或集合) ,所以你可以将一个方法的输出管道化到下一个方法的输入。这创建了一个_管道_,将 计算描述为一系列转换。
以下是管道逐步的工作方式:
开始:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].filter(fn(x int) bool { x % 2 == 0 })—— 只保留偶数:[2, 4, 6, 8, 10].map(fn(x int) int { x * x })—— 将每个数平方:[4, 16, 36, 64, 100].fold(0, fn(acc int, x int) int { acc + x })—— 求和:4 + 16 + 36 + 64 + 100 = 220print(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(...) | 将转换组合为管道 |
核心要点:
闭包是函数作为值 —— 你可以将它们存储在变量中、作为参数传递、从其他函数 返回。它们捕获其封闭作用域中的变量。
高阶函数实现抽象 ——
apply、make_multiplier和迭代器方法都是高阶函数。 它们将_什么_(转换)与_如何_(迭代逻辑)分离。迭代器是惰性的 ——
.iter()创建一个轻量级游标。.filter()和.map()在消耗性适配器如.fold()强制求值之前什么都不做。这使得链接变得高效。链接创建可读管道 ——
.filter().map().fold()读起来像一个句子:"过滤, 然后转换,然后归约。"每一步都是一个小型、可测试、可组合的单元。相同的模式适用于所有目标平台 —— 闭包和迭代器是通用的编程概念。Auto 将 它们转译为各目标平台惯用的等价形式:Rust(闭包和
Iteratortrait)、Python (lambda 和推导式)、C(函数指针和循环)、TypeScript(箭头函数和Array方法)。
闭包和迭代器在 Rust 中是零成本抽象——它们编译为与手写循环相同的机器代码。Auto 继承了这种性能模型。你获得了表达性强、可读性好的代码,而不会牺牲速度。
在下一章中,我们将探索 comptime —— Auto 的编译时元编程系统,它允许你在编译 时执行代码来生成类型、值甚至整个函数。