【问题标题】:Memoize a curried function记忆一个咖喱函数
【发布时间】:2019-06-02 06:47:16
【问题描述】:
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

【问题讨论】:

  • 我猜你应该添加一个不同的层来缓存调用......
  • 查看reselect 库。应该做你想做的开箱即用。
  • 为什么需要currified,似乎是一个不必要的抽象。

标签: javascript caching memoization currying


【解决方案1】:

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

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

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

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

  • C — 返回结果的 thunk
    (因此我们可以在计算之前检查是否已经拥有它。)

这支持柯里化和非柯里化函数,也支持将函数作为值返回的函数。

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 尚未定义。然后返回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 函数上的实际缓存集。枚举对象属性时不返回符号:

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>

【讨论】:

    【解决方案2】:

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

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

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

    内部函数接受一个参数并检查该值是否在地图中。

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

      • 如果是函数,则在函数上创建一个新闭包和一个新映射,

      • 如果没有函数获取结果,

      作为地图新元素的值。

    • 最终从地图中返回值。

    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; }

    【讨论】:

    • 请您详细说明一下您的答案(解释)很难理解所有这些功能是如何工作的;)
    • @robe007,你是说f = ...吗?那是一个带有一个参数的函数并返回一个带有另一个参数的函数,并返回相同的然后返回结果。基本上它是对参数的闭包。
    • 不错!我在这里使用了类似的技术-> github.com/tonix-tuft/react-suspense-async-effect
    • 又是一个好人!我刚刚注意到可以使用对象{} 而不是地图来完成(使用.hasOwnProperty(arg) 进行存在测试)。您使用new Map() 有什么特别的原因,还是一种解决方案与另一种解决方案一样好?
    • @cars10m,在地图中,键可以是任何类型,因为对象只知道字符串和符号。
    【解决方案3】:

    您不能将 map 传递给每个函数。 你可以这样做:

    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));
        }
      }
    }

    【讨论】:

      【解决方案4】:

      有趣的问题——你可以为每个函数设置独立的缓存。外部函数上的缓存将保存函数。每个内部函数都可以获得自己的独立缓存。因此调用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))

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-02-09
        • 2022-01-02
        • 2011-04-21
        相关资源
        最近更新 更多