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

函数式编程艺术 / 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 类型系统 — 类型如何保障程序正确性