Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
kolodny committed Mar 6, 2017
0 parents commit eda6b16
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.nyc_output
coverage
13 changes: 13 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
language: node_js
node_js:
- '1'
- '2'
- '3'
- '4'
- '4'
- '5'
- '6'
- '7'
script: "npm run test-travis"
# Send coverage data to Coveralls
after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js"
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
member-berry
===

[![NPM version][npm-image]][npm-url]
[![Build status][travis-image]][travis-url]
[![Test coverage][coveralls-image]][coveralls-url]
[![Downloads][downloads-image]][downloads-url]

### Memoize a function of n args, O(n) recall, and no memory leaks.

This function is similar to lodash.memoize, the main difference
is that it memoizes any number of arguments, makes sure not to
leak any memory while maintaining a complexity of
O(`arguments.length`).

## Usage

```js
import memeberBerry from 'member-berry';
var hashCode = function(str) {
var hash = 0, i, chr, len;
if (str.length === 0) return hash;
for (i = 0, len = str.length; i < len; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
function computeHash() {
console.log('doing a slow calculation');
var hash = 0
for (var i = 0; i < arguments.length; i++) {
hash = hashCode(hash + arguments[index])
}
return hash;
}
var memoized = memberBerry(computeHash);
memoized("test") // calculates
memoized("test") // doesn't recalculate

var obj = {};
memoized(obj) // calculates
memoized(obj) // doesn't recalculate

memoized("test", obj) // calculates
memoized("test", obj) // doesn't recalculate
```

### Implementation
The technique used to achive O(n) lookup, is to use a trie-like
data structure to store the cached values. Here's a basic
snippet with accompanying explaination:


```js
var concat = function(a, b, c) { return a + b + c; };
var memoized = memberBerry(concat);

memoized(1, 2, 3)
/* memoized internal cache looks something like this:
{
1: {
2: {
3: {
result: "123"
}
}
}
}
*/
```

`member-berry` uses weakmaps to avoid holding onto object
references longer than needed. Weakmaps can't use primitive
values as keys so there's also a "wrapped" object associated
with primitives.

[npm-image]: https://img.shields.io/npm/v/member-berry.svg?style=flat-square
[npm-url]: https://npmjs.org/package/member-berry
[travis-image]: https://img.shields.io/travis/kolodny/member-berry.svg?style=flat-square
[travis-url]: https://travis-ci.org/kolodny/member-berry
[coveralls-image]: https://img.shields.io/coveralls/kolodny/member-berry.svg?style=flat-square
[coveralls-url]: https://coveralls.io/r/kolodny/member-berry
[downloads-image]: http://img.shields.io/npm/dm/member-berry.svg?style=flat-square
[downloads-url]: https://npmjs.org/package/member-berry
28 changes: 28 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module.exports = memberBerry;

var resultObject = {};
function memberBerry(fn) {
var wrappedPrimitives = {};
var map = new WeakMap();
return function() {
var currentMap = map;
for (var index = 0; index < arguments.length; index++) {
var arg = arguments[index];
if (typeof arg !== 'object') {
var key = (typeof arg) + arg
if (!wrappedPrimitives[key]) wrappedPrimitives[key] = {};
arg = wrappedPrimitives[key];
}
var nextMap = currentMap.get(arg);
if (!nextMap) {
nextMap = new WeakMap();
currentMap.set(arg, nextMap);
}
currentMap = nextMap;
}
if (!currentMap.has(resultObject)) {
currentMap.set(resultObject, fn.apply(null, arguments));
}
return currentMap.get(resultObject);
}
}
23 changes: 23 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "member-berry",
"version": "1.0.0",
"description": "Memoize a function of n args, O(n) recall, and no memory leaks",
"main": "index.js",
"scripts": {
"test": "mocha",
"test-debug": "mocha --inspect --debug-brk",
"test-cov": "nyc npm test && nyc report --reporter=lcov",
"test-travis": "nyc npm test && nyc report --reporter=lcov"
},
"keywords": [
"memoize"
],
"author": "Moshe Kolodny",
"license": "MIT",
"devDependencies": {
"coveralls": "^2.12.0",
"expect": "^1.20.2",
"mocha": "^3.2.0",
"nyc": "^10.1.2"
}
}
95 changes: 95 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
var memeberBerry = require('./')
var expect = require('expect')

describe('memeber-berry', function() {
var id;
var func;
var membered;

beforeEach(function() {
id = 0;
func = function() { return ++id; };
membered = memeberBerry(func);
});

it('members one obj', function() {
var obj = {};
expect(membered(obj)).toEqual(1);
expect(membered(obj)).toEqual(1, 'ooh, I member');
});

it('members two different objs', function() {
var obj1 = {};
var obj2 = {};
expect(membered(obj1)).toEqual(1);
expect(membered(obj2)).toEqual(2);
expect(membered(obj1)).toEqual(1, 'ooh, I member');
expect(membered(obj2)).toEqual(2, 'ooh, I member');
});

it('members multiple args', function() {
var obj1 = {};
var obj2 = {};
expect(membered(obj1, obj2)).toEqual(1);
expect(membered(obj1, obj2)).toEqual(1, 'ooh, I member');
});

it('members different multiple args', function() {
var obj1 = {};
var obj2 = {};
expect(membered(obj1, obj2)).toEqual(1);
expect(membered(obj2, obj1)).toEqual(2);
expect(membered(obj1, obj2)).toEqual(1, 'ooh, I member');
expect(membered(obj2, obj1)).toEqual(2, 'ooh, I member');
});

it('members different similar primitives', function() {
expect(membered(1)).toEqual(1);
expect(membered(1)).toEqual(1, 'ooh, I member');
expect(membered('1')).toEqual(2);
});

it('is O(1)', function() {
var originalWeakMap = WeakMap;
var operations = 0;
WeakMap = function() {
var wm = new originalWeakMap();
return {
has: function(key) {
operations++;
return wm.has(key);
},
get: function(key) {
operations++;
return wm.get(key);
},
set: function(key, value) {
operations++;
return wm.set(key, value);
},
}
};
membered = memeberBerry(func);
var lastArgs;
func = function() {
lastArgs = [].slice.call(arguments);
return ++id;
};
var arraySameContents = function(arr1, arr2) {
expect(arr1.length).toEqual(arr2.length);
for (var index = 0; index < arr1.length; index++) {
expect(arr1[index]).toBe(arr2[index]);
}
};
membered = memeberBerry(func);
var objects = Array(100).join('.').split('.').map(function() { return {}; });
expect(membered.apply(null, objects)).toEqual(1);
expect(membered.apply(null, objects)).toEqual(1, 'ooh, I member');
arraySameContents(objects, lastArgs);
expect(operations).toBeLessThan(310);
expect(membered.apply(null, objects.concat({}))).toEqual(2);
expect(lastArgs.length).toNotEqual(objects.length);
expect(operations).toBeLessThan(420);
});

})

0 comments on commit eda6b16

Please sign in to comment.