函数式编程艺术 / 09 惰性求值
09 惰性求值
“懒惰是程序员的美德——只在需要时才计算。”
9.1 惰性求值概述
惰性求值(Lazy Evaluation) 是一种求值策略:表达式只在需要它的值时才被计算,并且只计算一次。这与严格求值(Eager Evaluation)相对——严格求值在绑定时立即计算。
9.1.1 求值策略对比
| 策略 | 何时计算 | 语言 |
|---|---|---|
| 严格求值 | 绑定时立即计算 | JavaScript, Python, Rust, Java |
| 惰性求值 | 需要时才计算 | Haskell |
| 短路求值 | 逻辑运算符的惰性 | 大多数语言的 && / ` |
9.1.2 惰性求值的好处
| 好处 | 说明 |
|---|---|
| 无限数据结构 | 可以定义无限列表、无限流 |
| 避免不必要计算 | 只计算实际需要的部分 |
| 组合更灵活 | 可以安全组合可能不会用到的计算 |
| 更好的模块化 | 生产者和消费者解耦 |
9.1.3 惰性求值的缺点
| 缺点 | 说明 |
|---|---|
| 空间泄漏 | 未求值的 thunk 占用内存 |
| 性能不可预测 | 计算可能在意外时刻发生 |
| 调试困难 | 调用栈和执行顺序不直观 |
| side effect 顺序 | 副作用的执行顺序不确定 |
9.2 Haskell 的惰性求值
Haskell 是唯一默认使用惰性求值的主流语言。
9.2.1 基本示例
-- 无限列表
naturals :: [Integer]
naturals = [1..] -- 无限列表,不会卡住
-- 只取需要的部分
take 5 naturals -- [1, 2, 3, 4, 5]
-- 无限的斐波那契数列
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
-- fibs = [0, 1, 1, 2, 3, 5, 8, 13, ...]
-- tail fibs= [1, 1, 2, 3, 5, 8, 13, ...]
-- zipWith = [0+1, 1+1, 1+2, 2+3, 3+5, ...]
take 10 fibs -- [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
-- 无限质数(埃拉托斯特尼筛法)
primes :: [Integer]
primes = sieve [2..]
where
sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
take 10 primes -- [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
-- 惰性求值的关键:未使用的参数不会被计算
const :: a -> b -> a
const x _ = x
-- const 42 (error "boom") → 42(不会报错,因为第二参数未使用)
9.2.2 Thunk(延迟计算)
在 Haskell 中,未求值的表达式称为 thunk。
-- GHCi 中查看 thunk
let x = 1 + 2 :: Int
:sprint x
-- x = _ (下划线表示 thunk)
-- 强制求值
let x = 1 + 2 :: Int
print x -- 3
:sprint x
-- x = 3
-- seq 强制求值
let x = 1 + 2 :: Int
x `seq` ()
-- x 现在被求值了
9.2.3 严格性控制
-- BangPatterns
{-# LANGUAGE BangPatterns #-}
-- 使用 ! 强制严格求值
strictSum :: [Int] -> Int
strictSum = go 0
where
go !acc [] = acc
go !acc (x:xs) = go (acc + x) xs
-- 使用 $! (严格应用)
foldl' :: (b -> a -> b) -> b -> [a] -> b
foldl' _ acc [] = acc
foldl' f acc (x:xs) = let acc' = f acc x
in acc' `seq` foldl' f acc' xs
-- 对比
foldl (+) 0 [1..1000000] -- 栈溢出(累积 thunk)
foldl' (+) 0 [1..1000000] -- 正常(严格求值)
9.3 模拟惰性求值
9.3.1 JavaScript 中的 Thunk
// 手动实现 thunk(延迟计算)
const thunk = (fn) => {
let computed = false;
let value;
return () => {
if (!computed) {
value = fn();
computed = true;
}
return value;
};
};
// 使用
const lazyValue = thunk(() => {
console.log('Computing...');
return 42;
});
// 此时不会计算
console.log('Before');
console.log(lazyValue()); // 计算并输出 42
console.log(lazyValue()); // 直接返回缓存值 42
9.3.2 Python 中的惰性求值
# 生成器是 Python 的惰性求值机制
def naturals():
n = 1
while True:
yield n
n += 1
# 取前 5 个
from itertools import islice
list(islice(naturals(), 5)) # [1, 2, 3, 4, 5]
# 无限斐波那契
def fibs():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
list(islice(fibs(), 10)) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# 使用延迟求值的惰性列表
class LazyList:
def __init__(self, generator):
self._gen = generator
self._cache = []
def __getitem__(self, index):
while len(self._cache) <= index:
self._cache.append(next(self._gen))
return self._cache[index]
# 使用
lazy_nats = LazyList(naturals())
lazy_nats[0] # 1(计算并缓存)
lazy_nats[4] # 5(继续计算到 index=4)
lazy_nats[2] # 3(从缓存返回)
9.3.3 Rust 中的惰性迭代器
// Rust 的迭代器默认惰性
let numbers: Vec<i32> = (1..)
.filter(|x| x % 2 == 0) // 惰性
.map(|x| x * x) // 惰性
.take(5) // 惰性
.collect(); // 触发计算
// numbers = [4, 16, 36, 64, 100]
// 自定义惰性迭代器
struct Fibonacci {
a: u64,
b: u64,
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let result = self.a;
let new_b = self.a + self.a;
self.a = self.b;
self.b = new_b;
Some(result)
}
}
fn fibonacci() -> Fibonacci {
Fibonacci { a: 0, b: 1 }
}
// 使用
let first_10: Vec<u64> = fibonacci().take(10).collect();
9.3.4 Clojure 中的惰性序列
;; Clojure 的序列默认惰性
(range) ;; 无限序列
(take 5 (range)) ;; (0 1 2 3 4)
;; 无限斐波那契
(defn fibs []
(map first (iterate (fn [[a b]] [b (+ a b)]) [0 1])))
(take 10 (fibs)) ;; (0 1 1 2 3 5 8 13 21 34)
;; 埃拉托斯特尼筛法
(defn primes []
(let [sieve (fn sieve [s]
(cons (first s)
(lazy-seq
(sieve (filter #(not= 0 (mod % (first s)))
(rest s))))))]
(sieve (range 2 Integer/MAX_VALUE))))
(take 10 (primes)) ;; (2 3 5 7 11 13 17 19 23 29)
;; lazy-seq 宏创建惰性序列
(defn fibonacci []
(letfn [(fib [a b]
(lazy-seq (cons a (fib b (+ a b)))))]
(fib 0 1)))
(take 10 (fibonacci)) ;; (0 1 1 2 3 5 8 13 21 34)
9.4 无限数据结构
9.4.1 无限列表
-- 自然数
nats :: [Integer]
nats = [1..]
-- 偶数
evens :: [Integer]
evens = [2, 4..]
-- 平方数
squares :: [Integer]
squares = map (^2) [1..]
-- 质数
primes :: [Integer]
primes = sieve [2..]
where sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
-- 组合无限列表
firstNSquaresThatAreEven :: Int -> [Integer]
firstNSquaresThatAreEven n = take n (filter even squares)
firstNSquaresThatAreEven 5 -- [4, 16, 36, 64, 100]
9.4.2 无限树
-- 无限二叉树
data Tree a = Node a (Tree a) (Tree a)
-- 无限的自然数树
natTree :: Tree Integer
natTree = go 1
where
go n = Node n (go (2*n)) (go (2*n + 1))
-- 按层级取值
level :: Int -> Tree a -> [a]
level 0 (Node x _ _) = [x]
level n (Node _ l r) = level (n-1) l ++ level (n-1) r
take 3 (level 0 natTree) -- [1]
take 3 (level 1 natTree) -- [2, 3]
take 3 (level 2 natTree) -- [4, 5, 6, 7]
9.5 Stream(流)
Stream 是惰性求值在实际编程中的常见形式。
9.5.1 概念对比
| 特性 | 数组/列表 | 流 (Stream) |
|---|---|---|
| 求值方式 | 立即(严格) | 延迟(惰性) |
| 内存 | 存储所有元素 | 只存储当前元素 |
| 长度 | 有限 | 可以无限 |
| 适合场景 | 需要随机访问 | 数据处理管道 |
9.5.2 JavaScript 中的 Stream
// 简化的 Stream 实现
class Stream {
constructor(head, tailFn) {
this.head = head;
this._tailFn = tailFn; // 惰性计算 tail
}
get tail() {
if (!this._tail) {
this._tail = this._tailFn();
}
return this._tail;
}
map(fn) {
return new Stream(fn(this.head), () => this.tail.map(fn));
}
filter(pred) {
if (pred(this.head)) {
return new Stream(this.head, () => this.tail.filter(pred));
}
return this.tail.filter(pred);
}
take(n) {
if (n <= 0) return [];
return [this.head, ...this.tail.take(n - 1)];
}
static from(n) {
return new Stream(n, () => Stream.from(n + 1));
}
}
// 无限自然数流
const nats = Stream.from(1);
nats.take(5); // [1, 2, 3, 4, 5]
// 变换和过滤(惰性执行)
const result = nats
.filter(x => x % 2 === 0)
.map(x => x * x)
.take(5);
// [4, 16, 36, 64, 100]
9.5.3 RxJS 流
import { range, filter, map, take, toArray } from 'rxjs';
// 无限数据流(处理到取消订阅为止)
const source$ = range(1, Infinity).pipe(
filter(x => x % 2 === 0),
map(x => x * x),
take(5),
toArray()
);
source$.subscribe(console.log);
// [4, 16, 36, 64, 100]
9.6 生成器(Generator)
生成器是 JavaScript 和 Python 中实现惰性求值的主要方式。
9.6.1 生成器基础
JavaScript:
// 生成器函数
function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}
// 使用
const gen = naturals();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
// 生成器组合
function* take(n, iterable) {
let count = 0;
for (const item of iterable) {
if (count >= n) return;
yield item;
count++;
}
}
function* filter(pred, iterable) {
for (const item of iterable) {
if (pred(item)) yield item;
}
}
function* map(fn, iterable) {
for (const item of iterable) {
yield fn(item);
}
}
// 组合使用
const result = [...take(5, filter(x => x % 2 === 0, map(x => x * x, naturals())))];
// [4, 16, 36, 64, 100]
Python:
# 生成器函数
def naturals():
n = 1
while True:
yield n
n += 1
# 生成器表达式
squares = (x * x for x in range(10))
# itertools 提供丰富的惰性操作
from itertools import islice, count, filterfalse, chain
# 无限流操作
even_squares = (x*x for x in count(1) if x % 2 == 0)
first_5 = list(islice(even_squares, 5))
# [4, 16, 36, 64, 100]
9.6.2 协程(Coroutine)
生成器可以用作协程——双向传递数据。
function* accumulator() {
let sum = 0;
while (true) {
const value = yield sum;
sum += value;
}
}
const acc = accumulator();
acc.next(); // { value: 0, done: false }(初始化)
acc.next(10); // { value: 10, done: false }
acc.next(20); // { value: 30, done: false }
acc.next(30); // { value: 60, done: false }
9.7 业务场景
9.7.1 日志文件处理
# 处理大型日志文件,惰性方式不会加载整个文件到内存
def read_lines(filepath):
with open(filepath, 'r') as f:
for line in f: # 逐行读取,惰性
yield line.strip()
def parse_log_line(line):
parts = line.split(' ', 3)
return {'timestamp': parts[0], 'level': parts[1], 'module': parts[2], 'message': parts[3]}
from itertools import islice
# 管道处理:读取 → 解析 → 过滤 → 取前 10 个错误
error_lines = (
parse_log_line(line)
for line in read_lines('/var/log/app.log')
)
errors = (
entry for entry in error_lines
if entry['level'] == 'ERROR'
)
top_10_errors = list(islice(errors, 10))
# 只处理需要的行,不会遍历整个文件
9.7.2 实时数据流处理
// 传感器数据流处理
function* sensorData(sensorId) {
while (true) {
yield {
sensorId,
timestamp: Date.now(),
value: Math.random() * 100,
};
}
}
// 移动平均
function* movingAverage(stream, windowSize) {
const window = [];
for (const data of stream) {
window.push(data.value);
if (window.length > windowSize) window.shift();
yield {
...data,
movingAvg: window.reduce((a, b) => a + b, 0) / window.length,
};
}
}
// 组合管道
const readings = sensorData('temp-001');
const smoothed = movingAverage(readings, 5);
// 消费流
for (const data of smoothed) {
if (data.movingAvg > 80) {
console.log('Temperature alert!', data);
}
}
9.8 注意事项
| 注意事项 | 说明 |
|---|---|
| 空间泄漏 | 保持 thunk 的引用导致内存无法释放 |
| seq 的使用 | 在 Haskell 中适时强制严格求值 |
| 调试困难 | 惰性代码的执行顺序不直观 |
| 内存上限 | 大数据流中使用 take 或分块处理 |
| 共享 vs 复制 | 惰性值共享计算结果(单例),注意并发 |
9.9 小结
| 要点 | 说明 |
|---|---|
| 惰性求值 | 只在需要时计算,支持无限数据结构 |
| Thunk | 未求值的表达式,计算后缓存结果 |
| Stream | 惰性数据流,逐元素处理 |
| 生成器 | JS/Python 中的惰性迭代机制 |
| 严格控制 | 使用 seq、$!、! 等控制求值时机 |
扩展阅读
下一章:10 类型系统 — 类型如何保障程序正确性