Created
January 15, 2019 08:49
-
-
Save orrgal1/9e95bf7566a0c874d2eb19287b440d06 to your computer and use it in GitHub Desktop.
Executable Decision Tree
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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