惰性求值 #

一、惰性求值概念 #

1.1 什么是惰性求值 #

惰性求值(Lazy Evaluation)是Haskell的核心特性:

  • 表达式只在需要时求值
  • 每个表达式最多求值一次
  • 支持无限数据结构

1.2 与严格求值对比 #

haskell
-- 严格求值(其他语言)
-- 所有参数在函数调用前求值

-- 惰性求值(Haskell)
-- 参数只在需要时求值

-- 示例
const :: a -> b -> a
const x _ = x

-- 在严格语言中,const 5 (error "boom") 会报错
-- 在Haskell中,const 5 (error "boom") 返回 5

1.3 Thunk #

haskell
-- Thunk是未求值的表达式
-- 它是一个指向代码和环境的指针

-- 示例
x = 1 + 2  -- x是一个thunk,尚未求值

-- 当需要x的值时,thunk被求值
y = x + 1  -- 触发x的求值

二、无限数据结构 #

2.1 无限列表 #

haskell
-- 无限列表
ones :: [Int]
ones = 1 : ones  -- [1, 1, 1, 1, ...]

-- 使用
take 5 ones  -- [1, 1, 1, 1, 1]

-- 自然数
nats :: [Int]
nats = 0 : map (+1) nats  -- [0, 1, 2, 3, ...]

-- 使用
take 10 nats  -- [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

2.2 斐波那契数列 #

haskell
-- 斐波那契数列
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

-- 使用
take 10 fibs  -- [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
fibs !! 10    -- 55

2.3 质数 #

haskell
-- 埃拉托斯特尼筛法
primes :: [Int]
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]

2.4 无限树 #

haskell
data Tree a = Node a (Tree a) (Tree a)

-- 无限二叉树
infiniteTree :: Tree Int
infiniteTree = go 1
  where
    go n = Node n (go (2 * n)) (go (2 * n + 1))

-- 使用
treeToList :: Int -> Tree a -> [a]
treeToList 0 _ = []
treeToList n (Node x left right) = 
    x : treeToList (n - 1) left ++ treeToList (n - 1) right

三、求值策略 #

3.1 Weak Head Normal Form (WHNF) #

haskell
-- WHNF:表达式被求值到最外层构造器

-- WHNF示例
(1 + 2, 3 + 4)  -- WHNF,元组已构造
Just (1 + 2)    -- WHNF,Just已构造
\x -> 1 + 2     -- WHNF,lambda已构造

-- 非WHNF示例
1 + 2           -- 不是WHNF
[1 + 2, 3 + 4]  -- 不是WHNF(列表元素未求值)

3.2 seq函数 #

haskell
-- seq :: a -> b -> b
-- 强制求值第一个参数到WHNF

-- 示例
seq (1 + 2) "done"  -- "done",但1+2被求值

-- 使用seq避免空间泄漏
strictSum :: [Int] -> Int
strictSum = go 0
  where
    go acc [] = acc
    go acc (x:xs) = acc `seq` go (acc + x) xs

3.3 BangPatterns #

haskell
{-# LANGUAGE BangPatterns #-}

-- 使用!强制严格求值
strictSum :: [Int] -> Int
strictSum = go 0
  where
    go !acc [] = acc
    go !acc (x:xs) = go (acc + x) xs

-- 记录中的严格字段
data Person = Person
    { name :: !String
    , age  :: !Int
    }

3.4 deepseq #

haskell
import Control.DeepSeq

-- deepseq:完全求值
deepseq :: NFData a => a -> b -> b

-- 示例
forceSum :: [Int] -> Int
forceSum xs = sum xs `deepseq` sum xs

-- NFData实例
instance NFData Int where
    rnf x = x `seq` ()

四、惰性的优势 #

4.1 模块化 #

haskell
-- 分离生成和处理
-- 生成器
generate :: [Int]
generate = [1..]

-- 处理器
process :: [Int] -> [Int]
process = take 10 . filter even . map (*2)

-- 组合
result = process generate  -- [4, 8, 12, 16, 20]

4.2 无限数据结构 #

haskell
-- 处理无限数据
takeWhile (< 100) [n^2 | n <- [1..]]
-- [1, 4, 9, 16, 25, 36, 49, 64, 81]

-- 查找第一个满足条件的元素
find :: (a -> Bool) -> [a] -> Maybe a
find p = listToMaybe . filter p

-- 使用
find (> 100) [n^2 | n <- [1..]]
-- Just 121

4.3 短路求值 #

haskell
-- 短路逻辑运算
False && undefined  -- False(不计算undefined)
True || undefined   -- True

-- 短路列表操作
take 0 undefined  -- []
null undefined    -- 错误!需要求值

五、惰性的问题 #

5.1 空间泄漏 #

haskell
-- 空间泄漏示例
badSum :: [Int] -> Int
badSum [] = 0
badSum (x:xs) = x + badSum xs

-- 问题:累积大量未求值的thunk
-- badSum [1..1000000] 会占用大量内存

-- 解决:使用严格求值
goodSum :: [Int] -> Int
goodSum = go 0
  where
    go !acc [] = acc
    go !acc (x:xs) = go (acc + x) xs

5.2 性能不可预测 #

haskell
-- 惰性求值可能导致性能不可预测
-- 有时很快,有时很慢

-- 示例
lazyLength :: [a] -> Int
lazyLength = length

-- 如果列表元素很大,可能触发大量求值

5.3 内存占用 #

haskell
-- foldl的空间问题
foldl (+) 0 [1..1000000]
-- 构建巨大的thunk:((((0 + 1) + 2) + 3) + ...)

-- 解决:使用foldl'
import Data.List (foldl')
foldl' (+) 0 [1..1000000]  -- 严格版本

六、控制求值 #

6.1 严格数据类型 #

haskell
-- 严格字段
data StrictPair = StrictPair !Int !Int

-- 所有字段严格
{-# LANGUAGE StrictData #-}
data Point = Point Double Double

6.2 严格函数 #

haskell
-- 使用seq
strictMap :: (a -> b) -> [a] -> [b]
strictMap _ [] = []
strictMap f (x:xs) = 
    let y = f x
    in y `seq` (y : strictMap f xs)

-- 使用BangPatterns
strictMap' :: (a -> b) -> [a] -> [b]
strictMap' _ [] = []
strictMap' f (!x:xs) = f x : strictMap' f xs

6.3 evaluate #

haskell
import Control.Exception (evaluate)

-- 在IO中强制求值
main :: IO ()
main = do
    let x = 1 + 2
    evaluate x  -- 强制求值x
    print x

七、实践示例 #

7.1 高效求和 #

haskell
import Data.List (foldl')

-- 严格求和
sum' :: [Int] -> Int
sum' = foldl' (+) 0

-- 严格平均值
average :: [Int] -> Double
average xs = fromIntegral (sum' xs) / fromIntegral (length xs)

7.2 文件处理 #

haskell
-- 惰性读取文件
processFile :: FilePath -> IO ()
processFile path = do
    contents <- readFile path  -- 惰性读取
    let result = process contents
    print result

-- 严格读取大文件
processLargeFile :: FilePath -> IO ()
processLargeFile path = do
    contents <- readFile path
    length contents `seq` print "File read"  -- 确保读取完成
    let result = process contents
    print result

7.3 流处理 #

haskell
-- 处理无限流
streamProcess :: [Int] -> [Int]
streamProcess = take 100 . filter even . map (*2)

-- 使用
streamProcess [1..]  -- [4, 8, 12, ..., 200]

八、总结 #

惰性求值要点:

  1. 概念:表达式只在需要时求值
  2. 无限数据结构:支持无限列表、树等
  3. WHNF:Weak Head Normal Form
  4. 控制求值seqBangPatternsdeepseq
  5. 优势:模块化、无限数据、短路求值
  6. 问题:空间泄漏、性能不可预测
  7. 解决:严格数据类型、严格函数、foldl'

掌握惰性求值后,你已经完成了Haskell核心概念的学习!

最后更新:2026-03-27