Skip to content

Commit

Permalink
optimization: merge identical premises
Browse files Browse the repository at this point in the history
  • Loading branch information
frankthelen committed Dec 30, 2017
1 parent 06111ee commit e9fe9e4
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 8 deletions.
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,59 @@ In some cases, it is desired to stop the engine as soon as a specific rule has f
This is achieved by settings the respective rules' property `final` to `true`.
Default, of course, is `false`.

### Optimization of premises (`when`)

It is very common that different rules partially share the same premises.
Rools will merge identical premises into one.
You are free to use references or just to repeat the same premise.
Both cases are working fine.

Example 1: by reference
```js
const isApplicable = facts => facts.user.salery >= 2000;
const rule1 = {
when: isApplicable,
...
};
const rule2 = {
when: isApplicable,
...
};
```

Example 2: repeat premise
```js
const rule1 = {
when: facts => facts.user.salery >= 2000,
...
};
const rule2 = {
when: facts => facts.user.salery >= 2000,
...
};
```

TL;DR

Technically, this is achieved by hashing the premises
(remember, a function is an object in JavaScript).
This can be a classic function or an ES6 arrow function.
This can be a reference or the function directly.
It's tested with Node 8 and 9 (see unit tests `premises.spec.js`).

```
const md5 = require('md5');
const hash1 = md5(facts => facts.user.salery > 2000);
const hash2 = md5(facts => facts.user.salery > 2000);
const hash3 = md5(facts => facts.user.salery > 3000);
console.log(hash1 === hash2); // true
console.log(hash1 === hash3); // false
```

### Todos

Some of the features on my list are:
* Conflict resolution by specificity
* Optimization: merge identical premises (`when`) into one
* Optimization: re-evaluate only those premises (`when`) that are relying on modified facts
* Support asynchronous actions (`then`)
* More unit tests
21 changes: 14 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const assert = require('assert');
const md5 = require('md5');
const actionId = require('uniqueid')('a');
const premiseId = require('uniqueid')('p');

Expand All @@ -8,6 +9,7 @@ class Rools {
} = { logErrors: true, logDebug: false, logDelegate: null }) {
this.actions = [];
this.premises = [];
this.premisesByHash = {};
this.maxSteps = 100;
this.logErrors = logErrors;
this.logDebug = logDebug;
Expand All @@ -30,13 +32,18 @@ class Rools {
this.actions.push(action);
const whens = Array.isArray(rule.when) ? rule.when : [rule.when];
whens.forEach((when) => {
const premise = {
id: premiseId(),
name: rule.name,
when,
};
action.premises.push(premise);
this.premises.push(premise);
const hash = md5(when); // is function already introduced by other rule?
let premise = this.premisesByHash[hash];
if (!premise) { // create new premise
premise = {
id: premiseId(),
name: rule.name,
when,
};
this.premisesByHash[hash] = premise; // add to hash
this.premises.push(premise); // add to premises
}
action.premises.push(premise); // add to action
});
});
}
Expand Down
125 changes: 125 additions & 0 deletions test/premises.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const Rools = require('../src');
require('./setup');

describe('Rules.register() / optimization of premises', () => {
it('should not merge premises if not identical', () => {
const rule1 = {
name: 'rule1',
when: facts => facts.user.name === 'frank',
then: () => {},
};
const rule2 = {
name: 'rule2',
when: facts => facts.user.name === 'michael',
then: () => {},
};
const rools = new Rools();
rools.register(rule1, rule2);
expect(rools.premises.length).to.be.equal(2);
});

it('should merge premises if identical / reference / arrow function', () => {
const isFrank = facts => facts.user.name === 'frank';
const rule1 = {
name: 'rule1',
when: isFrank,
then: () => {},
};
const rule2 = {
name: 'rule2',
when: isFrank,
then: () => {},
};
const rools = new Rools();
rools.register(rule1, rule2);
expect(rools.premises.length).to.be.equal(1);
});

it('should merge premises if identical / reference / classic function', () => {
function isFrank(facts) {
return facts.user.name === 'frank';
}
const rule1 = {
name: 'rule1',
when: isFrank,
then: () => {},
};
const rule2 = {
name: 'rule2',
when: isFrank,
then: () => {},
};
const rools = new Rools();
rools.register(rule1, rule2);
expect(rools.premises.length).to.be.equal(1);
});

it('should merge premises if identical / hash / arrow function', () => {
const rule1 = {
name: 'rule1',
when: facts => facts.user.name === 'frank',
then: () => {},
};
const rule2 = {
name: 'rule2',
when: facts => facts.user.name === 'frank',
then: () => {},
};
const rools = new Rools();
rools.register(rule1, rule2);
expect(rools.premises.length).to.be.equal(1);
});

it('should merge premises if identical / hash / classic function()', () => {
const rule1 = {
name: 'rule1',
when: function p(facts) {
return facts.user.name === 'frank';
},
then: () => {},
};
const rule2 = {
name: 'rule2',
when: function p(facts) {
return facts.user.name === 'frank';
},
then: () => {},
};
const rools = new Rools();
rools.register(rule1, rule2);
expect(rools.premises.length).to.be.equal(1);
});

it('should merge premises if identical / hash / slightly different', () => {
const rule1 = {
name: 'rule1',
when: facts => facts.user.name === 'frank',
then: () => {},
};
const rule2 = {
name: 'rule2',
when: facts => facts.user.name === "frank", // eslint-disable-line quotes
then: () => {},
};
const rools = new Rools();
rools.register(rule1, rule2);
expect(rools.premises.length).to.be.equal(1);
});

it('should merge premises if identical / with Date object', () => {
const date = new Date('2000-01-01');
const rule1 = {
name: 'rule1',
when: facts => facts.user.birthdate > date,
then: () => {},
};
const rule2 = {
name: 'rule2',
when: facts => facts.user.birthdate > date,
then: () => {},
};
const rools = new Rools();
rools.register(rule1, rule2);
expect(rools.premises.length).to.be.equal(1);
});
});

0 comments on commit e9fe9e4

Please sign in to comment.