强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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$!! 等控制求值时机

扩展阅读

  1. Lazy Evaluation - Haskell Wiki
  2. Stream Processing - Computerphile — 视频讲解
  3. ReactiveX - RxJS 文档 — 响应式流

下一章10 类型系统 — 类型如何保障程序正确性