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

函数式编程艺术 / 07 递归与不动点

07 递归与不动点

“递归是函数式编程中的循环——通过函数调用自身来表达重复计算。”


7.1 递归概述

递归(Recursion) 是函数调用自身的技术。在函数式编程中,由于不可变性,不能使用 for/while 循环修改计数器,递归成为表达重复计算的主要方式。

7.1.1 递归三要素

要素说明示例
基准情况不再递归的终止条件factorial 0 = 1
递归情况调用自身的规则factorial n = n * factorial (n-1)
收敛性递归必须趋向基准情况n 每次减 1,最终到达 0

7.1.2 基本递归示例

Haskell:

-- 阶乘
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

-- 斐波那契
fibonacci :: Integer -> Integer
fibonacci 0 = 0
fibonacci 1 = 1
fibonacci n = fibonacci (n-1) + fibonacci (n-2)

-- 列表长度
myLength :: [a] -> Int
myLength []     = 0
myLength (_:xs) = 1 + myLength xs

-- 列表反转
myReverse :: [a] -> [a]
myReverse []     = []
myReverse (x:xs) = myReverse xs ++ [x]

-- 快速排序
qsort :: Ord a => [a] -> [a]
qsort []     = []
qsort (p:xs) = qsort [x | x <- xs, x < p]
               ++ [p]
               ++ qsort [x | x <- xs, x >= p]

JavaScript:

// 阶乘
const factorial = (n) => n === 0 ? 1 : n * factorial(n - 1);

// 斐波那契
const fibonacci = (n) => n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);

// 列表长度
const length = (arr) => arr.length === 0 ? 0 : 1 + length(arr.slice(1));

// 快速排序
const qsort = (arr) => {
  if (arr.length <= 1) return arr;
  const [pivot, ...rest] = arr;
  return [
    ...qsort(rest.filter(x => x < pivot)),
    pivot,
    ...qsort(rest.filter(x => x >= pivot)),
  ];
};

Python:

# 阶乘
def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n - 1)

# 斐波那契
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 快速排序
def qsort(arr):
    if len(arr) <= 1:
        return arr
    pivot, *rest = arr
    return qsort([x for x in rest if x < pivot]) + [pivot] + qsort([x for x in rest if x >= pivot])

Rust:

// 阶乘
fn factorial(n: u64) -> u64 {
    match n {
        0 => 1,
        _ => n * factorial(n - 1),
    }
}

// 快速排序
fn qsort(arr: &[i32]) -> Vec<i32> {
    if arr.len() <= 1 {
        return arr.to_vec();
    }
    let pivot = arr[0];
    let rest = &arr[1..];
    let mut result = qsort(&rest.iter().copied().filter(|&x| x < pivot).collect::<Vec<_>>());
    result.push(pivot);
    result.extend(qsort(&rest.iter().copied().filter(|&x| x >= pivot).collect::<Vec<_>>()));
    result
}

Clojure:

;; 阶乘
(defn factorial [n]
  (if (zero? n) 1
    (* n (factorial (dec n)))))

;; 斐波那契
(defn fibonacci [n]
  (if (<= n 1) n
    (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))

;; 快速排序
(defn qsort [[pivot & rest]]
  (if (nil? pivot) []
    (concat (qsort (filter #(< % pivot) rest))
            [pivot]
            (qsort (filter #(>= % pivot) rest)))))

7.2 尾递归(Tail Recursion)

普通递归会在栈上保存每一层调用的信息,深度过大会导致栈溢出。尾递归通过将结果作为参数传递来避免这个问题。

7.2.1 尾递归 vs 普通递归

普通递归:
  factorial(4) → 4 * factorial(3)
                → 4 * 3 * factorial(2)
                → 4 * 3 * 2 * factorial(1)
                → 4 * 3 * 2 * 1 * factorial(0)
                → 4 * 3 * 2 * 1 * 1
                → 24
  栈: [f4, f3, f2, f1, f0]

尾递归:
  factorial(4, 1) → factorial(3, 4)
                  → factorial(2, 12)
                  → factorial(1, 24)
                  → factorial(0, 24)
                  → 24
  栈: [f(0,24)] (优化后,只需要一个栈帧)

7.2.2 尾递归实现

Haskell:

-- 普通递归(非尾递归)
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)  -- 乘法在递归调用之后

-- 尾递归(使用累加器)
factorial' :: Integer -> Integer
factorial' n = go n 1
  where
    go 0 acc = acc
    go n acc = go (n - 1) (n * acc)  -- 递归调用是最后一步

-- 使用 seq 强制严格求值,确保尾递归优化
factorial'' :: Integer -> Integer
factorial'' n = go n 1
  where
    go 0 acc = acc
    go n acc = acc `seq` go (n - 1) (n * acc)

-- foldl' 本身就是尾递归
factorial''' :: Integer -> Integer
factorial''' n = foldl' (*) 1 [1..n]

JavaScript:

// 尾递归版本
const factorial = (n, acc = 1) =>
  n === 0 ? acc : factorial(n - 1, n * acc);

// 蹦床函数(Trampoline)—— 在不支持 TCO 的引擎中实现尾递归
const trampoline = (fn) => {
  let result = fn;
  while (typeof result === 'function') {
    result = result();
  }
  return result;
};

const factorialTrampoline = (n, acc = 1) =>
  n === 0 ? acc : () => factorialTrampoline(n - 1, n * acc);

trampoline(() => factorialTrampoline(100000));  // 不会栈溢出

// 尾递归斐波那契
const fib = (n, a = 0, b = 1) =>
  n === 0 ? a : fib(n - 1, b, a + b);

Python:

# 尾递归版本
def factorial(n, acc=1):
    if n == 0:
        return acc
    return factorial(n - 1, n * acc)

# Python 不支持尾递归优化,使用装饰器模拟
import sys

class TailCallException(Exception):
    def __init__(self, args, kwargs):
        self.args = args
        self.kwargs = kwargs

def tail_call_optimized(func):
    def wrapper(*args, **kwargs):
        while True:
            try:
                return func(*args, **kwargs)
            except TailCallException as e:
                args = e.args
                kwargs = e.kwargs
    return wrapper

@tail_call_optimized
def factorial(n, acc=1):
    if n == 0:
        return acc
    raise TailCallException((n - 1, n * acc), {})

# 或者使用循环替代
def factorial_iter(n):
    acc = 1
    for i in range(1, n + 1):
        acc *= i
    return acc

Rust:

// Rust 编译器会自动进行尾调用优化(在某些情况下)
fn factorial(n: u64, acc: u64) -> u64 {
    match n {
        0 => acc,
        _ => factorial(n - 1, n * acc),
    }
}

// 迭代版本(Rust 风格)
fn factorial_iter(n: u64) -> u64 {
    (1..=n).product()
}

Clojure:

;; Clojure 使用 recur 进行尾递归优化
(defn factorial [n]
  (loop [n n acc 1]
    (if (zero? n) acc
      (recur (dec n) (* n acc)))))

;; 使用 reduce
(defn factorial-reduce [n]
  (reduce * 1 (range 1 (inc n))))

7.2.3 尾递归优化条件

条件说明
递归调用是最后一步递归调用的返回值直接作为函数的返回值
无待计算的表达式不能在递归调用后还有 n * 之类的操作
语言/编译器支持Haskell (GHC)、Rust、Scheme 支持;Python 不支持

7.3 递归消除技术

7.3.1 蹦床(Trampoline)

当语言不支持尾调用优化时,使用蹦床模式:

// 通用蹦床
const trampoline = (fn) => (...args) => {
  let result = fn(...args);
  while (typeof result === 'function') {
    result = result();
  }
  return result;
};

// 使用
const even = trampoline((n) => n === 0 ? true : () => odd(n - 1));
const odd = trampoline((n) => n === 0 ? false : () => even(n - 1));

even(1000000);  // true,不会栈溢出

7.3.2 CPS(Continuation-Passing Style)

-- CPS 变换
factorialCPS :: Integer -> (Integer -> r) -> r
factorialCPS 0 k = k 1
factorialCPS n k = factorialCPS (n-1) (\res -> k (n * res))

-- 使用
factorialCPS 5 id  -- 120
// JavaScript CPS
const factorialCPS = (n, k = (x) => x) =>
  n === 0 ? k(1) : factorialCPS(n - 1, (res) => k(n * res));

factorialCPS(5);  // 120

7.3.3 栈上迭代器(Stack-based Iteration)

# 手动栈管理
def flatten(nested):
    result = []
    stack = [nested]
    while stack:
        item = stack.pop()
        if isinstance(item, list):
            stack.extend(reversed(item))
        else:
            result.append(item)
    return result

7.4 分治策略(Divide and Conquer)

分治是递归的经典应用模式:将问题分解为子问题,递归求解,合并结果。

7.4.1 经典分治算法

归并排序:

mergeSort :: Ord a => [a] -> [a]
mergeSort []  = []
mergeSort [x] = [x]
mergeSort xs  = merge (mergeSort left) (mergeSort right)
  where
    (left, right) = splitAt (length xs `div` 2) xs

merge :: Ord a => [a] -> [a] -> [a]
merge [] ys = ys
merge xs [] = xs
merge (x:xs) (y:ys)
  | x <= y    = x : merge xs (y:ys)
  | otherwise = y : merge (x:xs) ys
def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)

def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    result.extend(left[i:])
    result.extend(right[j:])
    return result

二分查找:

binarySearch :: Ord a => [a] -> a -> Maybe Int
binarySearch xs target = go 0 (length xs - 1)
  where
    go lo hi
      | lo > hi   = Nothing
      | midVal == target = Just mid
      | midVal < target  = go (mid + 1) hi
      | otherwise        = go lo (mid - 1)
      where
        mid = (lo + hi) `div` 2
        midVal = xs !! mid

7.5 不动点(Fixed Point)

不动点是指满足 f(x) = x 的点 x。不动点是函数式编程中深层递归抽象的基础。

7.5.1 不动点组合子

Y 组合子:

Y = λf. (λx. f (x x)) (λx. f (x x))
Y g = g (Y g)  -- 不动点性质

Y 组合子在各语言中:

JavaScript:

// Y 组合子(非严格版本,使用惰性求值)
const Y = (f) => ((x) => f((y) => x(x)(y)))((x) => f((y) => x(x)(y)));

// 使用 Y 组合子定义阶乘
const factorialGen = (self) => (n) =>
  n === 0 ? 1 : n * self(n - 1);

const factorial = Y(factorialGen);
factorial(5);  // 120

// Z 组合子(严格版本)
const Z = (f) => ((x) => f((...args) => x(x)(...args)))((x) => f((...args) => x(x)(...args)));

Haskell:

-- Haskell 的 Y 组合子
-- 由于惰性求值,可以直接实现
fix :: (a -> a) -> a
fix f = f (fix f)

-- 使用 fix 定义阶乘
factorial :: Integer -> Integer
factorial = fix (\f n -> if n == 0 then 1 else n * f (n - 1))

-- fix 的展开
-- fix f = f (fix f) = f (f (fix f)) = f (f (f ...))

-- 使用 fix 定义斐波那契
fib :: Integer -> Integer
fib = fix (\f n -> if n <= 1 then n else f (n-1) + f (n-2))

Python:

# Y 组合子
Y = lambda f: (lambda x: f(lambda y: x(x)(y)))(lambda x: f(lambda y: x(x)(y)))

# 使用
factorial = Y(lambda f: lambda n: 1 if n == 0 else n * f(n - 1))
factorial(5)  # 120

fibonacci = Y(lambda f: lambda n: n if n <= 1 else f(n-1) + f(n-2))
fibonacci(10)  # 55

Clojure:

;; Y 组合子(使用延迟)
(defn Y [f]
  ((fn [x] (f (fn [& args] (apply (x x) args))))
   (fn [x] (f (fn [& args] (apply (x x) args))))))

;; 使用
(def factorial
  (Y (fn [f]
       (fn [n]
         (if (zero? n) 1
           (* n (f (dec n))))))))

(factorial 5)  ;; 120

7.5.2 不动点的实际应用

不动点组合子在实际编程中的应用:

应用说明
匿名递归不需要给函数命名就能递归
记忆化结合记忆化包装递归函数
解析器组合子解析递归语法结构
数据流分析迭代直到收敛

7.6 递归数据结构

7.6.1 二叉树

data Tree a = Leaf | Node (Tree a) a (Tree a)

-- 树的递归操作
treeSize :: Tree a -> Int
treeSize Leaf = 0
treeSize (Node left _ right) = 1 + treeSize left + treeSize right

treeMap :: (a -> b) -> Tree a -> Tree b
treeMap _ Leaf = Leaf
treeMap f (Node left val right) = Node (treeMap left) (f val) (treeMap right)

treeToList :: Tree a -> [a]
treeToList Leaf = []
treeToList (Node left val right) = treeToList left ++ [val] ++ treeToList right

-- 树折叠
foldTree :: (b -> a -> b -> b) -> b -> Tree a -> b
foldTree _ acc Leaf = acc
foldTree f acc (Node left val right) =
  f (foldTree f acc left) val (foldTree f acc right)
#[derive(Debug)]
enum Tree<T> {
    Leaf,
    Node { left: Box<Tree<T>>, value: T, right: Box<Tree<T>> },
}

impl<T> Tree<T> {
    fn size(&self) -> usize {
        match self {
            Tree::Leaf => 0,
            Tree::Node { left, right, .. } => 1 + left.size() + right.size(),
        }
    }

    fn map<U, F: Fn(&T) -> U>(&self, f: &F) -> Tree<U> {
        match self {
            Tree::Leaf => Tree::Leaf,
            Tree::Node { left, value, right } => Tree::Node {
                left: Box::new(left.map(f)),
                value: f(value),
                right: Box::new(right.map(f)),
            },
        }
    }
}

7.6.2 链表

data List a = Nil | Cons a (List a)

-- 链表操作全部是递归
myLength :: List a -> Int
myLength Nil = 0
myLength (Cons _ xs) = 1 + myLength xs

myAppend :: List a -> List a -> List a
myAppend Nil ys = ys
myAppend (Cons x xs) ys = Cons x (myAppend xs ys)

myReverse :: List a -> List a
myReverse = go Nil
  where
    go acc Nil = acc
    go acc (Cons x xs) = go (Cons x acc) xs

7.7 业务场景

7.7.1 文件系统遍历

import os
from pathlib import Path

def find_files_recursive(directory, pattern='*.py'):
    """递归查找文件"""
    results = []
    for item in Path(directory).iterdir():
        if item.is_file() and item.match(pattern):
            results.append(item)
        elif item.is_dir():
            results.extend(find_files_recursive(item, pattern))
    return results

# 函数式风格
from functools import reduce

def find_files_fp(directory, predicate):
    def walk(acc, path):
        if path.is_file():
            return acc + [path] if predicate(path) else acc
        elif path.is_dir():
            return reduce(walk, path.iterdir(), acc)
        return acc

    return reduce(walk, Path(directory).iterdir(), [])

7.7.2 JSON 处理

// 递归处理嵌套 JSON
const transformValues = (obj, fn) => {
  if (Array.isArray(obj)) {
    return obj.map(item => transformValues(item, fn));
  }
  if (obj !== null && typeof obj === 'object') {
    return Object.fromEntries(
      Object.entries(obj).map(([key, value]) => [key, transformValues(value, fn)])
    );
  }
  return fn(obj);
};

// 使用:将所有字符串值转小写
const data = {
  name: 'ALICE',
  address: { city: 'BEIJING', streets: ['MAIN ST', 'SECOND AVE'] },
};

transformValues(data, v => typeof v === 'string' ? v.toLowerCase() : v);
// { name: 'alice', address: { city: 'beijing', streets: ['main st', 'second ave'] } }

7.8 注意事项

注意事项说明
栈溢出深度递归需要尾递归优化或蹦床
性能尾递归优化后性能接近循环
记忆化递归函数可缓存中间结果
转换优先在支持循环的语言中,简单递归可转为迭代
收敛性确保递归趋向基准情况,避免无限递归

7.9 小结

要点说明
递归函数调用自身,替代命令式循环
尾递归递归调用是最后一步,可优化为循环
蹦床在不支持 TCO 的语言中模拟尾递归
分治分解→递归→合并 的经典模式
不动点Y/Z 组合子实现匿名递归

扩展阅读

  1. Tail Recursion - Haskell Wiki
  2. The Y Combinator — Mike Vanier
  3. 《The Little Schemer》 — Friedman & Felleisen(递归思维训练)

下一章08 Monad 与函子 — 函数式编程的"大 boss"