记忆一个咖喱函数

Memoize a curried function

const f = (arg1) => (arg2) => { /* returns something */ }

是否可以记忆 f 关于 2 个参数,即:

f(1)(2);
f(1)(3); // Cache not hit
f(4)(2); // Cache not hit
f(1)(2); // Cache hit

有趣的问题 — 您可以为每个函数设置独立的缓存。外部函数的缓存将保存函数。每个内部函数都可以获得自己独立的缓存。因此调用 f(10)(1) 后跟 f(10)(2) 将导致调用内部函数的缓存版本。再次调用 f(10)(1) 会命中两个缓存:

function getCachedF() {
  // outer cache holds functions keyed to argument
  let outer_memo = {}  
                
  const f = (arg1) => {
    if (!outer_memo.hasOwnProperty(arg1)) {
      // Create inner function on outer cache
      // each inner function needs its own cache
      // because it will return different values
      // given different outer function calls
      let inner_memo = {}                  
      console.log("outer cache miss")
      
      outer_memo[arg1] = (arg2) => {
        // just a normal memoized function
        // cache is simple key:value pair
        if (!inner_memo.hasOwnProperty(arg2)) {
          console.log("inner cache miss")
          inner_memo[arg2] = arg1 + arg2
        }
        return inner_memo[arg2]
      }
    }
    return outer_memo[arg1]
  }
  return f
}

let f = getCachedF()
// both caches miss
console.log("3+5", f(3)(5))

// cached result
console.log("3+5", f(3)(5))

// only inside cache hit
console.log("3+8", f(3)(8))

// inside cache only hits if both args are the same
console.log("10+8", f(10)(8))

另一种选择是使用单个缓存,其键是两个参数的组合,但是内部函数总是必须被调用。

您可以将 Map 作为缓存并为所有以下参数获取嵌套映射。

此缓存适用于任意数量的参数并重用之前调用的值。

它通过采用柯里化函数和可选的 Map 来工作。如果未提供映射,则会创建一个新映射,用作 returned 闭包或最终结果的所有其他调用的基本缓存。

内部函数采用单个参数并检查此值是否在映射中。

  • 如果不是,调用curried函数并检查returned值

    • if 函数,在函数上创建一个新的闭包和一个新的映射,

    • 如果没有函数取结果,

    作为地图新元素的值。

  • 最后return来自地图的值。

const cached = (fn, map = new Map()) => arg => {
    const inCache = map.has(arg);
    const hint = inCache ? 'in cache' : 'not in cache';

    console.log(arg, hint);

    if (!inCache) {
        const value = fn(arg);
        const result = typeof value === 'function' ? cached(value, new Map()) : value;

        map.set(arg, result);
    }

    return map.get(arg);
};

const f = a => b => c => a * b * c; // the original curried function
const g = cached(f); // its cached variant

console.log(g(1)(2)(5)); // not not not 10
console.log(g(1)(3)(4)); //  in not not 12
console.log(g(4)(2)(3)); // not not not 24
console.log(g(1)(2)(6)); //  in  in not 12
console.log(g(4)(2)(3)); //  in  in  in 24
.as-console-wrapper { max-height: 100% !important; top: 0; }

您不能将地图传递给每个函数。 你可以像下一个那样做:

const memoize = fn => {
  const cache = {};
  return (...args) => {
    const curriedFn = fn(...args);
    return (...next) => {
      const key = // generate your key
      if (key in cache) return cache[key];
      return (cache[key] = curriedFn(...next));
    }
  }
}

这可能不是规范的记忆功能。

需要缓存其结果的函数被赋予一个 cache 函数,用于存储和检索以前的结果:

const sum = memo(cache => a => b => cache(`${a}+${b}`, () => a + b));
//               ^^^^^                    ^^^^^^^^^^^  ^^^^^^^^^^^
//               A                        B            C
  • Acache 函数由 memo 函数提供。
    (如有必要,记忆函数可以选择不缓存某些结果。)

  • B — 结果的唯一键。 (例如 cache['1+2'] = 3

  • Cthunk 结果 return
    (所以我们可以在计算之前检查是否已经有了它。)

这既支持柯里化和非柯里化函数,也支持 return 作为值的函数。

memo函数可以实现如下:

const memo = fn => {
  const ns = Symbol();
  const cache = (key, thunk) => cache[ns][key] ??= thunk();
  cache[ns] = {};
  return fn(cache);
};

我非常喜欢用于管理缓存的 logical nullish assignment 运算符:

a ??= answer()

当且仅当 a 尚未定义时,右侧的表达式被计算并分配给 a 。然后它 return 是 a 的值:

const answer = () => (console.log('returning the answer'), 42);

let a;

a ??= answer();
//=> LOG: returning the answer
//=> 42

a ??= answer();
//=> 42

a ??= 40;
//=> 42

我使用了一个符号来隐藏 cache 函数中设置的实际缓存。枚举对象的属性时,符号未 returned:

const foo = {};
const key1 = Symbol();
const key2 = 'bar';

foo[key1] = 42;
foo[key2] = 41;

Object.keys(foo);
//=> ['bar']

Object.entries(foo);
//=> [['bar', 41]]

演示

// curried memoized function
const sum = memo(cache => a => b =>
  cache(`${a}+${b}`,
    () => (console.log(`computing ${a}+${b}…`), a+b)));
  
console.log(sum(1)(2));
console.log(sum(1)(2));
console.log(sum(1)(2));

// non-curried memoized function
const mul = memo(cache => (a, b) =>
  cache(`${a}*${b}`,
    () => (console.log(`computing ${a}*${b}…`), a*b)));
  
console.log(mul(2, 3));
console.log(mul(2, 3));
console.log(mul(2, 3));

// function-returning function
const deferred_sum = memo(cache => a => b =>
  cache(`${a}+${b}`,
    () => (console.log(`defer computing ${a}+${b}…`), () => a+b)));
    
console.log(deferred_sum(1)(2)());
console.log(deferred_sum(1)(2)());
console.log(deferred_sum(1)(2)());
<script>
const memo = fn => {
  const ns = Symbol();
  const cache = (key, thunk) => cache[ns][key] ??= thunk();
  cache[ns] = {};
  return fn(cache);
};
</script>