← 中文资源

简体中文版本

记忆化

记忆化以参数为键缓存纯函数结果,把重复计算变成查表——无需失效,因为相同输入永远产出相同输出。

阅读英文原文

概念定义

纯函数的历史就是一张查找表。记忆化在函数运行时把这张表物化:首次调用计算,之后调用直读。递归密集的算法——斐波那契、解析、树编辑距离——从指数塌缩到线性。

工作机制

包装器从参数计算缓存键——身份、结构哈希或序列化——命中即返回缓存值。弱键映射让条目随参数一起蒸发,避免泄漏;语言级变体包括带容量上限的 lru_cache 与按结构键控的编译器记忆表。

使用场景

参数集合重复的纯昂贵计算:递归求解器、派生数据选择器、纯组件渲染输出。不纯或 I/O 密集的函数要先重构成“纯核心加副作用外壳”再谈记忆化。

常见误区

不纯函数(读时钟、读可变状态)记忆化后会拿昨天的答案。键语义决定一切:对象身份键在结构相等时 miss;序列化键顺序不稳定则永远 miss。有界缓存需要淘汰策略,miss 路径要计入收益评估。异常与错误要不要缓存必须有显式策略——缓存一次失败通常就是错的。

代码示例

export interface CacheLookup<V> {
  hit: boolean;
  value?: V;
}

interface CacheEntry<V> {
  value: V | null;
  expiresAt: number;
}

export class NegativeCache<K, V> {
  private readonly entries = new Map<K, CacheEntry<V>>();
  private readonly ttlMs: number;
  private readonly now: () => number;

  constructor(ttlMs: number, now: () => number = () => Date.now()) {
    if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
      throw new RangeError("ttlMs must be a positive finite number");
    }
    if (typeof now !== "function") throw new TypeError("now must be a function");
    this.ttlMs = ttlMs;
    this.now = now;
  }

  get(key: K): CacheLookup<V> {
    const entry = this.entries.get(key);
    if (!entry) return { hit: false };
    if (this.now() >= entry.expiresAt) {
      this.entries.delete(key);
      return { hit: false };
    }
    if (entry.value === null) return { hit: true };
    return { hit: true, value: entry.value };
  }

  setMiss(key: K): void {
    this.entries.set(key, { value: null, expiresAt: this.now() + this.ttlMs });
  }

  setValue(key: K, value: V): void {
    if (value === null) throw new TypeError("value must not be null; use setMiss");
    this.entries.set(key, { value, expiresAt: this.now() + this.ttlMs });
  }

  delete(key: K): boolean {
    return this.entries.delete(key);
  }

  get size(): number {
    return this.entries.size;
  }
}

export async function withNegativeCache<K, V>(
  cache: NegativeCache<K, V>,
  key: K,
  loader: () => Promise<V | null>,
): Promise<V | null> {
  if (typeof loader !== "function") throw new TypeError("loader must be a function");
  const cached = cache.get(key);
  if (cached.hit) return cached.value ?? null;
  const value = await loader();
  if (value === null) cache.setMiss(key);
  else cache.setValue(key, value);
  return value;
}
export function memoize(fn, ttlMs = 60000) {
  const cache = new Map();
  return function memoized(key, ...rest) {
    const now = Date.now();
    const entry = cache.get(key);
    if (entry && now - entry.at < ttlMs) {
      return entry.value;
    }
    const value = fn(key, ...rest);
    cache.set(key, { value, at: now });
    return value;
  };
}