【问题标题】:Can a memoize function use a ES6 Map vs a hashtable?memoize 函数可以使用 ES6 Map 与哈希表吗?
【发布时间】:2017-04-11 21:06:27
【问题描述】:

前言:尝试通过转换一个利用哈希表的简单记忆来学习ES6 Maps

问题:

可以将起始对象替换为new Map(),如果可以,如何?并且有什么优势吗?如果不是,为什么?


说明:

这是一个带有函数 (add) 和起始对象 ({}) 的备忘录。在调用 memoized add (mAdd) 时,参数为 spread。最后,测试/设置哈希索引并返回值。

LINK TO CODE

const memo = (fn, hash) => (...a) => {
  return hash[a] === void 0 ?  hash[a] = fn(...a) : `${hash[a]} memo`;
};

const add = (x, y) =>  x + y;

const mAdd = memo(add, {});


console.log(mAdd(2,2));
console.log(mAdd(2,2));

不适用于地图:

const memo = (fn, map) => (...a) => {
  return map.get(a) === void 0 ?  map.set(a, fn(...a)) : `${map.get(a)} memo`;
};

const add = (x, y) =>  x + y;

const mAdd = memo(add, new Map());


console.log(mAdd(2,2));
console.log(mAdd(2,2));

【问题讨论】:

  • if so, how? - 用new Map()替换{}
  • @JaromandaX 这不是那么容易吗? repl.it/E9DG/1我试过了
  • 我也是如此 - 它对我有用,奇怪的是
  • 您可能还想查看为此目的的 WeakMaps
  • 您能创建一个答案吗?我仍然不确定标记为正确的问题是否确实是最佳答案。

标签: javascript dictionary ecmascript-6 hashtable memoization


【解决方案1】:

问题是 Map 使用对象的引用来标识键(如果提供的键是对象而不是原始类型)。在这种情况下,a 变量是一个数组,即使它在每次调用时都具有相同的值,但该变量的引用并不相同,因此 Map 将其理解为新键。看看底部代码。

const test = new Map();
const a = ['bla'];
test.set(a, 'b');
console.log(test.get(a));
const f = ['bla'];
console.log(test.get(f));

解决此问题的一种方法是将 a 变量字符串化。

const memo = (fn, hash) => (...a) => {
  const key = JSON.stringify(a);
  if (hash.has(key)) {
    return `${hash.get(key)} memo`;        
  }
  const val = fn(...a);
  hash.set(key, val);
  return val;
};

const add = (x, y) =>  x + y;

const mAdd = memo(add, new Map());


console.log(mAdd(2,2));
console.log(mAdd(2,2));

注意:您的代码根本不可读。所以我不得不对其进行一些编辑以使其更易于理解。

【讨论】:

  • 这是我缺少的密钥创建和 .has 方法!也感谢您的参考!
  • “对象不能用作 Map 中的键”它们可以。
  • @zeroflagL 你有更好的答案吗?我真的不喜欢将参数字符串化以创建密钥
  • @zeroflagL 是对的。 Matthew Harwood 看看我更新的帖子。
  • @zeroflagL 是您不想将参数字符串化以创建密钥,那么您可以将传入的对象中的唯一属性替换为对象本身,例如{uid: "123",name:"joe"} 你会使用 "123"。即使对于基元,连接一堆字符串来创建一个整体键也非常慢。请参阅 iMemoized 以获取使用常规 JavaScript 对象进行缓存的索引记忆的示例。
【解决方案2】:

主要问题是参数不代表同一个对象。它们的内容可以,这就是字符串化会起作用的原因。

使用对象作为哈希,也执行一种字符串化:创建属性2,2。 (附带说明:这不是完整的证明,因为内容是扁平的。参数[1,[2,3]][1,2,3] 都将创建属性[1,2,3]

但是,由于Map 实际上在某种程度上更智能,所以对象本身被用作键,并且每次调用都会为参数创建一个新对象。

使用相同的参数调用会起作用,但当然会降低函数的用处:

var pars = [2,2];
console.log(mAdd(pars));
console.log(mAdd(pars));

(必须将方法签名更改为const memo = (fn, map) => (a) => { 才能使上述方法生效。还要注意Map.set 返回地图对象本身,而不是正在设置的值)。

最简单的实现是对键进行字符串化。最安全的是JSON.stringify 来处理所有情况,但是如果您对内容比较确定,则可以改为使用join 之类的操作:

const memo = (fn, map) => (...a) => {
    const key = a.join(',');
  if(map.has(key))
        return `${map.get(key)} memo`;
    let res = fn(...a);
  map.set(key, res );
  return res;
};

可以通过多种方式创建密钥。 stringify 是可能的,甚至const key = uneval(a); 可以根据长度和内容创建某种哈希整数,但其可靠性取决于可能的内容。例如如果已知值永远不会超过 100 并且参数的数量不会过长,则可以使用 const key =createKey(a); 调用像 const createKey = ([a1,...a]) => a.length ? a1 + 100 * createKey(a) : a1; 这样的助手

当然,对于示例而言,直接添加总是比创建密钥和查找密钥要快,但一般来说,创建密钥的方法是决定因素。

我意识到我可能没有在所有这些中说任何新内容,但底线是传递的参数不代表同一个对象。也就是说,我想建议另一种选择:创建一个分支地图。包含子映射(第一个参数作为键)到结果(第二个参数作为键)或后续映射到第二个元素的基本映射。

edit 上述分支的一个示例(单个映射可用于不同功能以减少内存占用):

const memoMap = new Map(); //use a general map for branches for all memoized functions, because result is stored in the child function as the key

const memo = (fn) => (...a) => {
  let key, r = a, map = memoMap;
  while(r.length){
      [key,...r] = r;      
      if(map.has(key))
      	map = map.get(key);
      else
      	map.set(key, map = new Map());
  }
  let res = map.get(fn); //get result for this specific function
  if(res===undefined)
  	map.set(fn, res = fn(...a));
 	else return `${res} memo`; //<-- line for testing purposes
  return res;
};


const add = (x, y) =>  x + y,
  subtr = (x,y) => x - y,
  mAdd = memo(add);

console.log(mAdd(2,2));
console.log(mAdd(2,2));
console.log(memo(subtr)(2,2));
console.log(memo(subtr)(2,2));

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-12
    • 2010-10-02
    • 1970-01-01
    • 1970-01-01
    • 2016-03-25
    • 2011-02-27
    相关资源
    最近更新 更多