Skip to content

Instantly share code, notes, and snippets.

@orrgal1
Created January 15, 2019 08:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save orrgal1/9e95bf7566a0c874d2eb19287b440d06 to your computer and use it in GitHub Desktop.
Save orrgal1/9e95bf7566a0c874d2eb19287b440d06 to your computer and use it in GitHub Desktop.
Executable Decision Tree
// a work in progress...
const autoBind = require('auto-bind');
/**
* A Tree of decision making executable nodes.
*/
class Tree {
/**
* @constructor {String} name.
*/
constructor() {
this.top = new Node();
autoBind(this);
}
/**
* Sets the logger
* @param {Object} logger - the logger implementation to use.
* @returns {Tree} this.
*/
withLogger(logger) {
this.logger = logger;
return this;
}
/**
* Sets an extraction function that extracts an overriding item to be used instead of the item passed in to exec.
* @param {Function} forItem - a function that extracts a new item provided the main item
* @returns {Tree} this.
*/
for(forItem) {
this.forItem = forItem;
return this;
}
/**
* @param {Object} item - the item to be processed and acted upon.
* @returns {Object} deep clone of the item after processing.
*/
async exec(item) {
try {
if (this.logger) this.logger.debug(`starting tree`);
} catch (e) {
console.error(e);
}
return await this.top.exec(await (this.forItem(item) || item));
}
}
/**
* A decision making Node.
*/
class Node {
/**
* @constructor {String} name - name of the node.
*/
constructor(name) {
this.name = name;
autoBind(this);
}
/**
* Sets the condition for the node
* @param {Function} condition - evaluates if this node should execute.
* @returns {Node} this
*/
on(condition) {
this.condition = typeof condition === 'function' ? condition : () => condition;
return this;
}
/**
* Sets an always true condition
* @returns {Node} this
*/
always() {
this.condition = () => true;
return this;
}
/**
* Sets the threshold for a randomly diverging node
* @param {Number} thresh - the threshold under which to qualify.
* @returns {Node} this
*/
rand(thresh) {
this.thresh = thresh;
return this;
}
/**
* Sets the actions to be performed
* @param {Function} actions - functions that act on the item.
* @returns {Node} this
*/
do(...actions) {
this.actions = actions;
return this;
}
/**
* Sets the next nodes
* @param {Node[]} nextNodes - the next nodes to be evaluated.
* @returns {Node} this
*/
next(...nextNodes) {
this.nextNodes = nextNodes;
return this;
}
doAction(item) {
return async action => {
if (this.logger) this.logger.debug(`doing ${action.name}`);
await action(item);
};
}
isDiverged() {
return this.nextNodes[0].thresh >= 0;
}
async diverge(item) {
let rand = Math.random();
if (this.logger) this.logger.debug(`diverging on ${rand}`);
for (const nextNode of this.nextNodes) {
if ((rand -= nextNode.thresh) <= 0) {
if (this.logger) this.logger.debug(`matched at ${nextNode.thresh}`);
return await nextNode.exec(item);
} else if (this.logger) this.logger.debug(`skipped ${nextNode.thresh}`);
}
}
async evalConditions(item) {
for (const nextNode of this.nextNodes) {
if (this.logger) {
this.logger.debug(`testing ${nextNode.condition.name || nextNode.condition()}`);
}
if (nextNode.condition(item)) {
if (this.logger) {
this.logger.debug(`matched ${nextNode.condition.name || nextNode.condition()}`);
}
return await nextNode.exec(item);
}
}
}
/**
* Executes the node
* @param {Object} item - the item being processed.
* @returns {Object} the deep clone of the item.
*/
async exec(item) {
// perform actions
if (this.actions) {
if (typeof this.actions === 'function') {
this.actions = this.actions(item);
}
this.actions.forEach(await this.doAction(item));
}
// eval next node
if (this.nextNodes) {
return await (this.isDiverged() ? this.diverge(item) : this.evalConditions(item));
}
}
/**
* Sets the logger
* @param {Object} logger - the logger implementation to use.
* @returns {Node} this.
*/
withLogger(logger) {
this.logger = logger;
return this;
}
}
/**
* factory function for Node.
* @param {Function} conditionFn - a function that prepares and evaluates if this node should execute.
* @returns {Node} a new node initialized with the condition function.
*/
function on(conditionFn) {
return new Node().on(conditionFn);
}
/**
* factory function for an always Node.
* @returns {Node} a new node initialized with the condition function.
*/
function always() {
return new Node().always();
}
/**
* factory function for randomly diverging Node.
* @param {Number} thresh - the threshold under which to qualify.
* @returns {Node} a new node initialized with the thresh property.
*/
function rand(thresh) {
return new Node().rand(thresh);
}
/**
* factory function for Tree.
* @returns {Tree} a new tree.
*/
function tree(nodes) {
const tree = new Tree();
if (nodes) tree.top.next(...nodes);
return tree;
}
const forItem = item => nodes => {
return tree(nodes).for(item).exec;
};
module.exports.classes = {
Tree,
Node,
};
module.exports.factories = {
on,
always,
tree,
rand,
forItem,
};
module.exports.factories.logged = logger => ({
on: condition => on(condition).withLogger(logger),
always: () => always().withLogger(logger),
tree: nodes => tree(nodes).withLogger(logger),
rand: thresh => rand(thresh).withLogger(logger),
forItem: item => nodes => forItem(item)(nodes).withLogger(logger),
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment