记忆一个咖喱函数

Posted

技术标签:

【中文标题】记忆一个咖喱函数【英文标题】: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,似乎是一个不必要的抽象。 【参考方案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))

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

【讨论】:

以上是关于记忆一个咖喱函数的主要内容,如果未能解决你的问题,请参考以下文章

我应该如何制作功能咖喱?

Argo 错误:对成员“咖喱”的含糊提及

第一次咖喱牛肉饭

我的美味午餐---咖喱土豆鸡块

text 咖喱

javascript JS:挑选对象属性,咖喱