fools
v0.1.7
Published
A functional rules system based on the promise pattern
Downloads
17
Readme
This is an attempt to use the promise pattern to create functional rules system in Javascript.
Each of the classes take one or more functions as constructor arguments and return a function. The resulting function always takes a single argument.
Why?
Ordinary functions and logic flow structures are immutable; once a function is defined it is a closed system. Foools components can be adjusted on the fly; you can change the fork's 'then' clause at any time, or it's else clause.
It is also very easy to load process structures from configuration files, and compose them with each other.
Error trapping
Almost every Fools function has an optional error function that will trap (most) errors in execution and respond to the output error. The result of the error trapper is returned in the place that the original function is called. Note, no effort is made by Fools to determine which context the error came from -- if that is important, you will want to catch and pipe errors yourself and add context hints to them.
To add error handling to a fools function call fools_function.err(function(err){...})
.
Fools.all
Fools.all calls each function in its roster with the input value. They are called in order and any errors emitted by a function are trapped and emitted as one composite error.
Like until
there is a my_all.last(last_fn)
method that is always done after the added methods.
Fools.all is (barring errors) functionally identical to map.
var my_all = Fools.all(fn_a, fn_b, fn_c...)
and/or
var my_all = Fools.all().add(fn_a).add(fn_b).add(fn_c)...
Fools.each
each
has a set of tests and true is returned when all of the tests pass.
It is the equivalent of _.every
.
var each = Fools.each()
.add(_.isNumber)
.add(function (n) {
return n > 0
})
.add(function (n) {
return !(n % 2)
});
_.each(['g', -4, 0, 1, 2, 3, 4, {}], function(n){
console.log('"%s" is positive odd number: %s', n, each(n));
});
/**
"g" is positive odd number: false
"-4" is positive odd number: false
"0" is positive odd number: false
"1" is positive odd number: false
"2" is positive odd number: true
"3" is positive odd number: false
"4" is positive odd number: true
"[object Object]" is positive odd number: false
*/
Fools.fork(test:{function}, if_true: {function (optional)}, if_false: {function (optional)}) : function
Fork is functionally identical to the "if" (or a ? b : c) statement.
Fork takes one function that returns true or false and calls the second function (with the original argument) if the first result is true, and the third function if the first function returns false.
var my_fork = Fools.fork(test, if_true_fn, if_else_fn)
or
var my_fork = Fools.Fork(test).then(if_true_fn).else(if_false_fn)
returns a function that can be called repeatedly with arguments; whether or not the true test is passed determines whether the true function or the false function is called (also, with those arguments).
Note because Fools.fork
returns a function (not an instance) you can nest forks for a binary branching
expert system.
You can also call my_fork.err(on_err_fn)
to create an error trapping function that receives any errors emitted by
the test or either of its branches.
Example
function if_odd(n){
return (n % 2);
}
function ifPositive(n){
return n > 0;
}
var test = Fools.fork(ifPositive)
.then(
Fools.fork(if_odd)
.then(function(n){ return 2 * n})
.else(_.identity) // if even
).else(function(){ return 0; }); // if not positive
console.log(_.map(_.range(-5, 6), test));
// result: [ 0, 0, 0, 0, 0, 0, 2, 2, 6, 4, 10 ]
Fools.gauntlet : {function}
Gauntlet is similar to until in that a series of tests are run until one of them is true. Unlike until, gauntlet returns an arbitrary value from the truthy test -- the return value of the test function is not related to the truthiness of the test. Gauntlet calls a series of tests on an input until one of them is true, and returns that tests' result.
In order for a function to both return true/false and return the maximum range of results, the tests are passed a second parameter, isGood; call this method to validate the result.
var bot_loc = {x: 0, y: 2};
var min_x = 0;
var max_x = 2;
var min_y = 0;
var max_y = 2;
var gauntlet = Fools.gauntlet()
.add(function (input, good) {
if (bot_loc.y > min_y) {
good();
bot_loc.y -= 1;
}
return 'N';
}).add(function (input, good) {
if (bot_loc.x < max_x) {
good();
bot_loc.x += 1;
}
return 'E';
});
gauntlet.if_last = function () {
return '0';
};
it ('should move north', function(){
gauntlet().should.eql('N');
bot_loc.should.eql({x: 0, y: 1});
});
it ('should move north again', function(){
gauntlet().should.eql('N');
bot_loc.should.eql({x: 0, y: 0});
});
it('should move east', function(){
gauntlet().should.eql('E');
bot_loc.should.eql({x: 1, y: 0});
});
it('should move east again', function(){
gauntlet().should.eql('E');
bot_loc.should.eql({x: 2, y: 0});
});
it('should not move', function(){
gauntlet().should.eql('0');
bot_loc.should.eql({x: 2, y: 0});
});
Fools.loop(iterator(iter: {Object}) : {function}): {function}
This is a method of walking a multidimensional matrix. The loop function has a similar profile to reduce: it takes a memo argument that is passed as the second argument to the iterator (the first being an amalgam of the dimensions being walked.
Use of a memo / the return value of loop is optional.
console.log(
Fools.loop(function(iter, memo){
memo.push(_.clone(iter));
return memo;
})
.dim('i', -1, 1)
.dim('j', -1, 1)([])
);
/** responds
[ { i: -1, j: -1 },
{ i: 0, j: -1 },
{ i: 1, j: -1 },
{ i: -1, j: 0 },
{ i: 0, j: 0 },
{ i: 1, j: 0 },
{ i: -1, j: 1 },
{ i: 0, j: 1 },
{ i: 1, j: 1 } ]
*/
note -- the iterator is an object that is changed from iteration to iteration -- saving its value requires use of clone or some similar method of extracting the values of the iter parameter.
Fools.range
Range is a function that takes a value and executes a function over it depending on where it falls in a numeric range of possible values. It is useful for examples to take a range of values and sorting them into bins.
Range is configured by calling .add(min_value, result_function)
multiple times to define how you want to respond
to a value that is >= the min but less than all larger min_values set. min_value
can be any numeric value.
the .add_min
and .add_max
methods allow for handling values outside the defined range.
note - the handler for the last bracket is never called; use .add_max
to handle the last bracket.
var negatives = [];
var positives = [];
var small = 0;
var large = 1;
var range = Fools.range()
.add(-10, function (n) {
negatives.push(n);
})
.add(0, function (n) {
positives.push(n)
})
.add(11, function () {
large++;
})
.add_min(function () {
small++;
})
.add_max(function () {
throw new Error('never called');
});
// call range 1000 times.
_.each(_.range(0, 1000), function () {
// call range with a random value in the -15 ... 15 range.
range(Math.round(Math.random() * 30 - 15));
});
console.log('small: %s', small);
console.log('large: %s', large);
console.log('negatives: %s', _.sortBy(negatives, _.identity));
console.log('positives: %s', _.sortBy(positives, _.identity));
/**
results similar to
small: 150
large: 165
negatives: -10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
positives: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10
*/
Fools.until
Fools executes its test one by one until one of them fails.
you can stack any number of functions in Fools.until chains. For any given input, the functions are called in order until one of them returns true.
until returns the index of the function that retrned true;
You can force a given function to be executed last by calling my_until.last(last_fn)
.
Errors can be trapped as with fork.
If no function returns true, until throws an error -- and that error will NOT be trapped by your error trapper.
var my_until = Fools.until(fn_a, fn_b, fn_c...)
and/or
var my_until = Fools.until().add(fn_a).add(fn_b).add(fn_c)...
Example
var Fools = require('./../fools');
var _ = require('lodash');
var until = Fools.until()
.add(function (n) {
return !_.isNumber(n);
})
.add(function (n) {
return n < 0
})
.add(function (n) {
return (n % 2);
})
.add(function(n){
return true;
});
_.each(['g', -4, 0, 1, 2, 3, 4, {}], function (n) {
var index = until(n);
if ( index == 3){
console.log('%s is a positive even number', n);
} else {
console.log('"%s" failed at test: %s', n, index);
}
});
/**
"g" failed at test: 0
"-4" failed at test: 1
0 is a positive even number
"1" failed at test: 2
2 is a positive even number
"3" failed at test: 2
4 is a positive even number
"[object Object]" failed at test: 0
*/
Fools.rate
rate
is a very specialized method to rank items on a weighted curve; it keeps a database
in closure that allows you to poll for the best and worst candidates.
You configure the rate function by adding properties to be considered, using the prop
method:
function letterToNumber(grade){
switch(grade){
case 'A':
return 4;
break;
case 'B':
return 3;
break;
case 'F':
return 0;
break;
default:
return 0;
}
}
rate.prop('science') // returns the science rating unfiltered
rate.prop('science', letterToNumber) // returns the numeric value of a letter grade
rate.prop('science', null, 2) // returns 2 * the value of the grade
rate.prop('science', letterToNumber, 2) returns 2 * the numeric value of a letter grade
Calling rate(target)
returns a numeric rating of a target.
calling rate.add(target)
adds the target to an internal collection for the purpose
of enabling the best()
, select(min, max)
, and worst()
methods.
Here are some examples of the rate system in action:
var util = require('util');
var rate = Fools.rate()
.prop('brains')
.prop('looks', null, 5);
var peter = {brains: 2, looks: 8, birth_year: 2014 - 45, name: 'Peter Griffin'};
var lois = {brains: 6, looks: 12, birth_year: 2014 - 43, name: 'Lois Griffin'};
var stewie = {brains: 14, looks: 5, birth_year: 2014 - 2, name: 'Stewie Griffin'};
var brian = {brains: 8, looks: 6, birth_year: 2014 - 8, name: 'Brian Griffin'};
var meg = {brains: 4, looks: 3, birth_year: 2014 - 16, name: 'Meg Griffin'};
console.log(' ----------- rating (looks biased) --------- ');
_.each([peter, lois, stewie, brian, meg], function (item) {
console.log('rating of %s: %s', item.name, rate(item));
console.log(' (%s * 5 + %s) / %s', item.looks, item.brains, rate.scale());
rate.add(item);
});
var best = (rate.best());
console.log('best: %s (%s)', best.data.name, best.rating);
var worst = (rate.worst());
console.log('worst: %s (%s)', worst.data.name, worst.rating);
console.log(' ----------- rating (brains biased) --------- ');
var rate2 = Fools.rate()
.prop('brains', null, 5)
.prop('looks');
_.each([peter, lois, stewie, brian, meg], function (item) {
console.log('rating of %s: %s', item.name, rate2(item)); // echoes the rating but doesn't record the candidate
rate2.add(item); // records the candidate for relative comparison
});
best = (rate2.best());
console.log('best: %s (%s)', best.data.name, best.rating);
worst = (rate2.worst());
console.log('worst: %s (%s)', worst.data.name, worst.rating);
console.log(' ----------- rating (age biased) --------- ');
var rate2 = Fools.rate()
.prop('brains', null, 2)
.prop('birth_year', function(birth_year){
return 2014 - birth_year
}, 3)
.prop('looks');
_.each([peter, lois, stewie, brian, meg], function (item) {
console.log('rating of %s: %s', item.name, rate2(item)); // echoes the rating but doesn't record the candidate
console.log(' (%s * 2 + %s * 3 + %s) / %s',
item.brains,
2014 - item.birth_year,
item.looks,
rate.scale());
rate2.add(item); // records the candidate for relative comparison
});
best = (rate2.best());
console.log('best: %s (%s)', best.data.name, best.rating);
worst = (rate2.worst());
console.log('worst: %s (%s)', worst.data.name, worst.rating);