Code Monkey home page Code Monkey logo

nools's Introduction

Deprecation Warning

C2FO is no longer maintaining this project. Please use accordingly. If you would like to help maintain or take over the project please let us know.

Build Status

browser support

Nools

Join the chat at https://gitter.im/C2FO/nools

Nools is a rete based rules engine written entirely in javascript.

Installation

npm install nools

Or download the source (minified)

Usage

Resources

Defining a flow

When using nools you define a flow which acts as a container for rules that can later be used to get a session

Programmatically

var nools = require("nools");

var Message = function (message) {
    this.text = message;
};

var flow = nools.flow("Hello World", function (flow) {

    //find any message that is exactly hello world
    flow.rule("Hello", [Message, "m", "m.text =~ /^hello\\sworld$/"], function (facts) {
        facts.m.text = facts.m.text + " goodbye";
        this.modify(facts.m);
    });

    //find all messages then end in goodbye
    flow.rule("Goodbye", [Message, "m", "m.text =~ /.*goodbye$/"], function (facts) {
        console.log(facts.m.text);
    });
});

In the above flow definition 2 rules were defined

  • Hello
    • Requires a Message
    • The messages's text must match the regular expression /^hello\\sworld$/
    • When matched the message's text is modified and then we let the engine know that we modified the message.
  • Goodbye
    • Requires a Message
    • The messages's text must match the regular expression /.*goodbye$/(anything that ends in goodbye)
    • When matched the resulting message is logged.

DSL

You may also use the nools rules language to define your rules.

The following is the equivalent of the rules defined programmatically above.

define Message {
    text : '',
    constructor : function(message){
        this.text = message;
    }
}

//find any message that starts with hello
rule Hello {
    when {
        m : Message m.text =~ /^hello(\s*world)?$/;
    }
    then {
        modify(m, function(){this.text += " goodbye";});
    }
}

//find all messages then end in goodbye
rule Goodbye {
    when {
        m : Message m.text =~ /.*goodbye$/;
    }
    then {
        console.log(m.text);
    }
}

To use the flow

var flow = nools.compile(__dirname + "/helloworld.nools"),
    Message = flow.getDefined("message");

Flow Events

Each flow can have the following events emitted.

  • assert (fact) - emitted when facts are asserted
  • retract (fact) - emitted when facts are retracted
  • modify (fact) - emitted when facts are modified
  • fire (name, rule) - emitted when an activation is fired.
session.on("assert", function(fact){
    //fact was asserted
});

session.on("retract", function(fact){
    //fact was retracted
});

session.on("modify", function(fact){
    //fact was modifed
});

session.on("fire", function(name, rule){
    //a rule was fired.
});

nools.compile

The compile method accepts the following parameters

  • source|path - The first argument must either be a path that ends in .nools or a string which is the source of the rules that you wish to compile.
  • options?
    • name : This is the name of the flow. You can use this name to look up the flow by using nools.getFlow.
    • define : A hash of Classes that should be aviable to the rules that you are compiling.
    • scope: A hash of items that should be available to rules as they run. (i.e. a logger)
  • cb? - an options function to invoke when compiling is done.

Example

rule "person name is bob" {
    when {
        p : Person p.name == 'bob';
    }
    then {
        logger.info("Found person with name of bob");
        retract(p);
    }
}

In the above rules file we make use of a Person class and a logger. In order for nools to properly reference the Class and logger you must specify them in your options.

var flow = nools.compile("personFlow.nools", {
    define: {
        //The person class the flow should use
        Person: Person
    },
    scope: {
        //the logger you want your flow to use.
        logger: logger
    }
});

You may also compile source directly.

var noolsSource = "rule 'person name is bob' {"
    + "   when {"
    + "     p : Person p.name == 'bob';"
    + "   }"
    + "   then {"
    + "       logger.info('Found person with name of bob');"
    + "       retract(p);"
    + "   }"
    + "}";

var flow = nools.compile(noolsSource, {
    define: {
        //The person class the flow should use
        Person: Person
    },
    scope: {
        //the logger you want your flow to use.
        logger: logger
    },
    name: 'person name is bob'
});

Working with a session

A session is an instance of the flow that contains a working memory and handles the assertion, modification, and retraction of facts from the engine.

To obtain an engine session from the flow invoke the getSession method.

var session = flow.getSession();

Working with facts

Facts are items that the rules should try to match.

Assert

To add facts to the session use assert method.

session.assert(new Message("hello"));
session.assert(new Message("hello world"));
session.assert(new Message("goodbye"));

As a convenience any object passed into getSession will also be asserted.

Note assert is typically used pre engine execution and during the execution of the rules.

flow.getSession(new Message("hello"), new Message("hello world"), new Message("goodbye"));

Retract

To remove facts from the session use the retract method.

var m = new Message("hello");

//assert the fact into the engine
session.assert(m);

//remove the fact from the engine
session.retract(m);

Note retract is typically used during the execution of the rules.

Modify

To modify a fact use the modify method.

Note modify will not work with immutable objects (i.e. strings).

var m = new Message("hello");

session.assert(m);

m.text = "hello goodbye";

session.modify(m);

Note modify is typically used during the execution of the rules.

Retrieving Facts

To get a list of facts currently in the session you can use the getFacts() method exposed on a session.

session.assert(1);
session.assert("A");
session.assert("B");
session.assert(2);

session.getFacts(); //[1, "A", "B", 2];

You may also pass in a Type to getFacts which will return facts only of the given type.

session.assert(1);
session.assert("A");
session.assert("B");
session.assert(2);

session.getFacts(Number); //[1, 2];
session.getFacts(String); //["A", "B"];

Firing the rules

When you get a session from a flow no rules will be fired until the match method is called.

var session = flow.getSession();
//assert your different messages
session.assert(new Message("goodbye"));
session.assert(new Message("hello"));
session.assert(new Message("hello world"));

//now fire the rules
session.match(function(err){
    if(err){
        console.error(err.stack);
    }else{
        console.log("done");
    }
})

The match method also returns a promise that is resolved once there are no more rules to activate.

session.match().then(
  function(){
      console.log("Done");
  },
  function(err){
    //uh oh an error occurred
    console.error(err.stack);
  });

Fire until halt

You may also run the engine an a "reactive" mode which will continue to match until halt is invoked.

In the following example the rules engine continues to evaluate until the counter reaches 10000. If you remove the "counted to high" rule then the engine would run indefinitely.

define Counter {
    count: 0,
    constructor: function(count){
        this.count = count;
    }
}

//We reached our goal
rule "I can count!" {
    when {
        $ctr: Counter $ctr.count == 10000;
    }
    then{
        console.log("Look ma! I counted to " + $ctr.count);
        halt();
    }
}

//no counter was asserted so create one
rule "not count" {
    when {
        not($ctr: Counter);
    }
    then{
        console.log("Imma gonna count!");
        assert(new Counter(1));
    }
}

//A little status update
rule "give them an update" {
    when{
        $ctr: Counter $ctr.count % 1000 == 0 {count: $count}
    }
    then{
        console.log("Imma countin...");
        modify($ctr, function(){this.count = $count + 1;});
    }
}

//just counting away
rule count {
    when{
        $ctr: Counter {count: $count}
    }
    then{
        modify($ctr, function(){
          this.count = $count + 1;
        });
    }
}
flow.getSession().matchUntilHalt(function(err){
    if(err){
        console.log(err.stack);
        return;
    }
    //halt finally invoked
});

matchUntilHalt also returns a promise.

flow.getSession().matchUntilHalt()
    .then(
        function(){
            //all done!
        },
        function(err){
            console.log(err.stack);
        }
    );

Disposing of the session

When working with a lot of facts it is wise to call the dispose method which will purge the current session of all facts, this will help prevent the process from growing a large memory footprint.

session.dispose();

Removing a flow

To remove a defined flow from nools use the deleteFlow function.

var myFlow = nools.flow("flow");

nools.deleteFlow("flow"); //returns nools for chaining

nools.getFlow("flow"); //undefined

You may also remove a flow using the FlowContainer object returned from nools.flow;

var myFlow = nools.flow("flow");

nools.deleteFlow(myFlow); //returns nools for chaining

nools.getFlow("flow"); //undefined

Removing All Flows

To remove all flow from nools use the deleteFlows function.

var myFlow = nools.flow("flow");

nools.deleteFlows(); //returns nools for chaining

nools.getFlow("flow"); //undefined

Checking If A Flow Exists

To check if a flow currently is registering with nools use the hasFlow function;

var myFlow = nools.flow("flow");

nools.hasFlow("flow"); //true

Agenda Groups

Agenda groups allow for logical groups of rules within a flow.

The agenda manages a stack of agenda-groups that are currently in focus. The default agenda-group is called main and all rules that do not have an agenda-group specified are placed into the main agenda-group.

As rules are fired and a particular agenda-group runs out of activations then that agenda-group is popped from the internal agenda-group stack and the next one comes into focus. This continues until focus is explicitly called again or the main agenda-group comes into focus.

Note Once an agenda group loses focus it must be re-added to the stack in order for those activations to be focused again.

To add a rule to an agenda-group you can use the agendaGroup option.

this.rule("Hello World", {agendaGroup: "ag1"}, [Message, "m", "m.name == 'hello'"], function (facts) {
    this.modify(facts.m, function () {
        this.name = "goodbye";
    });
});

this.rule("Hello World2", {agendaGroup: "ag2"}, [Message, "m", "m.name == 'hello'"], function (facts) {
    this.modify(facts.m, function () {
        this.name = "goodbye";
    });
});

Or in the dsl

rule "Hello World" {
    agenda-group: "ag1";
    when{
        m : Message m.name === 'hello';
    }
    then{
        modify(m, function(){
            this.name = "goodbye";
        });
    }
}
rule "Hello World 2" {
    agenda-group: "ag2";
    when{
        m : Message m.name === 'goodbye';
    }
    then {
        modify(m, function(){
            m.name = "hello";
        });
    }
}

In the above rules we have defined two agenda-groups called ag1 and ag2

Focus

When running your rules and you want a particular agenda group to run you must call focus on the session of the flow and specify the agenda-group to add to the stack.

//assuming a flow with the rules specified above.
var fired = [];
flow.getSession(new Message("hello"))
    .focus("ag1")
    .on("fire", function (ruleName) {
        fired.push(ruleName);
    })
    .match(function () {
        console.log(fired);  //[ 'Hello World' ]
    });

Or you can add multiple agenda-groups to the focus stack.

var fired1 = [], fired2 = [];
flow
    .getSession(new Message("goodbye"))
    .focus("ag1")
    .focus("ag2")
    .on("fire", function (ruleName) {
        fired1.push(ruleName);
    })
    .match(function () {
        console.log("Example 1", fired1); //[ 'Hello World', 'Hello World2' ]
    });
flow
    .getSession(new Message("hello"))
    .focus("ag2")
    .focus("ag1")
    .on("fire", function (ruleName) {
        fired2.push(ruleName);
    })
    .match(function () {
        console.log("Example 2", fired2); //[ 'Hello World2', 'Hello World' ]
    });

Notice above that the last agenda-group focused is added to the array first.

Auto Focus

Sometimes you may want an agenda-group to auto-focus whenever a certain rule is activated.

this.rule("Bootstrap", [State, "a", "a.name == 'A' && a.state == 'NOT_RUN'"], function (facts) {
    this.modify(facts.a, function () {
        this.state = 'FINISHED';
    });
});

this.rule("A to B",
    [
        [State, "a", "a.name == 'A' && a.state == 'FINISHED'"],
        [State, "b", "b.name == 'B' && b.state == 'NOT_RUN'"]
    ],
    function (facts) {
        this.modify(facts.b, function () {
            this.state = "FINISHED";
        });
    });

this.rule("B to C",
    {agendaGroup: "B to C", autoFocus: true},
    [
        [State, "b", "b.name == 'B' && b.state == 'FINISHED'"],
        [State, "c", "c.name == 'C' && c.state == 'NOT_RUN'"]
    ],
    function (facts) {
        this.modify(facts.c, function () {
            this.state = 'FINISHED';
        });
        this.focus("B to D");
    });

this.rule("B to D",
    {agendaGroup: "B to D"},
    [
        [State, "b", "b.name == 'B' && b.state == 'FINISHED'"],
        [State, "d", "d.name == 'D' && d.state == 'NOT_RUN'"]
    ],
    function (facts) {
        this.modify(facts.d, function () {
        this.state = 'FINISHED';
    });
});

Or using the dsl

rule Bootstrap {
    when{
        a : State a.name == 'A' && a.state == 'NOT_RUN';
    }
    then{
        modify(a, function(){
            this.state = 'FINISHED';
        });
    }
}


rule 'A to B' {
    when{
        a : State a.name == 'A' && a.state == 'FINISHED';
        b : State b.name == 'B' && b.state == 'NOT_RUN';
    }
    then{
        modify(b, function(){
            this.state = 'FINISHED';
        });
    }
}

rule 'B to C' {
    agenda-group: 'B to C';
    auto-focus: true;
    when{
        b: State b.name == 'B' && b.state == 'FINISHED';
        c : State c.name == 'C' && c.state == 'NOT_RUN';
    }
    then{
        modify(c, function(){
            this.state = 'FINISHED';
        });
        focus('B to D')
    }
}

rule 'B to D' {
    agenda-group: 'B to D';
    when{
        b: State b.name == 'B' && b.state == 'FINISHED';
        d : State d.name == 'D' && d.state == 'NOT_RUN';
    }
    then{
        modify(d, function(){
            this.state = 'FINISHED';
        });
    }
}

In the above rules we created a state machine that has a rule with auto-focus set to true.

This allows you to not have to specify focus when running the flow.

var fired = [];
flow
    .getSession(
        new State("A", "NOT_RUN"),
        new State("B", "NOT_RUN"),
        new State("C", "NOT_RUN"),
        new State("D", "NOT_RUN")
    )
    .on("fire", function (name) {
        fired.push(name);
    })
    .match()
    .then(function () {
        console.log(fired); //["Bootstrap", "A to B", "B to C", "B to D"]
    });

Conflict Resolution

When declaring a flow it is defined with a default conflict resolution strategy. A conflict resolution strategy is used to determine which rule to activate when multiple rules are ready to be activated at the same time.

Resolution Strategies

  • salience - sort activations on the specified salience. (NOTE The default salience of a rule is 0).
  • activationRecency - sort activations on activation recency. This is a LIFO strategy the latest activation takes precedence.
  • factRecency - sort activations based on fact recency. Each time a fact is asserted or modified its recency is incremented.
  • bucketCounter - sort activations on the internal bucket counter. The bucket counter is incremented after an activation is fired and the internal workingMemory is altered.

The default conflict resolution strategy consists of salience and activationRecency.

Examples

Example 1

//activation 1
{
    salience: 0,
    activationRecency: 1
}

//activation 2
{
    salience: 0,
    activationRecency: 2
}

In the above example activation 2 would be fired since it is the most recent activation and the rule salience is the same.

Example 2

//activation 1
{
    salience: 1,
    activationRecency: 1
}

//activation 2
{
    salience: 0,
    activationRecency: 2
}

In this example activation 1 would fire because it has a greater salience

Overidding The Default Strategy

To override the default strategy you can use the conflictResolution method on a flow.

var flow = nools.flow(/**define your flow**/);

flow.conflictResolution(["salience", "factRecency", "activationRecency"]);

The combination of salience, factRecency, and activationRecency would do the following.

  1. Check if the salience is the same, if not use the activation with the greatest salience.
  2. If salience is the same check if fact recency is the same. The fact recency is determined by looping through the facts in each activation and until two different recencies are found. The activation with the greatest recency takes precendence.
  3. If fact recency is the same check the activation recency.

Example 1

//activation 1
{
    salience: 2,
    factRecency: [1,2,3],
    activationRecency: 1
}

//activation 2
{
    salience: 1,
    factRecency: [1,2,4],
    activationRecency: 2
}

In this example activation 1 would fire because it's salience is the greatest.

Example 2

//activation 1
{
    salience: 1,
    factRecency: [1,2,3],
    activationRecency: 1
}

//activation 2
{
    salience: 1,
    factRecency: [1,2,4],
    activationRecency: 2
}

In Example 2 activation 2 would fire because of the third recency entry.

Example 3

//activation 1
{
    salience: 2,
    factRecency: [1,2,3],
    activationRecency: 1
}

//activation 2
{
    salience: 2,
    factRecency: [1,2,3],
    activationRecency: 2
}

In Example 3 activation 2 would fire because salience and factRecency are the same but activation 2's activation recency is greater.

Defining rules

Rule structure

Lets look at the "Calculate" rule in the Fibonacci example

   //flow.rule(type[String|Function], constraints[Array|Array[[]]], action[Function]);
   flow.rule("Calculate", [
         //Type     alias  pattern           store sequence to s1
        [Fibonacci, "f1",  "f1.value != -1", {sequence:"s1"}],
        [Fibonacci, "f2", "f2.value != -1 && f2.sequence == s1 + 1", {sequence:"s2"}],
        [Fibonacci, "f3", "f3.value == -1 && f3.sequence == s2 + 1"],
        [Result, "r"]
    ], function (facts) {
        var f3 = facts.f3, f1 = facts.f1, f2 = facts.f2;
        var v = f3.value = f1.value + facts.f2.value;
        facts.r.result = v;
        this.modify(f3);
        this.retract(f1);
    });

Or using the nools DSL

rule Calculate{
    when {
        f1 : Fibonacci f1.value != -1 {sequence:s1};
        f2 : Fibonacci f2.value != -1 && f2.sequence == s1 + 1 {sequence:s2};
        f3 : Fibonacci f3.value == -1 && f3.sequence == s2 + 1;
    }
    then {
       modify(f3, function(){
            this.value = f1.value + f2.value;
       });
       retract(f1);
    }
}

Salience

Salience is an option that can be specified on a rule giving it a priority and allowing the developer some control over conflict resolution of activations.

this.rule("Hello4", {salience: 7}, [Message, "m", "m.name == 'Hello'"], function (facts) {
});

this.rule("Hello3", {salience: 8}, [Message, "m", "m.name == 'Hello'"], function (facts) {
});

this.rule("Hello2", {salience: 9}, [Message, "m", "m.name == 'Hello'"], function (facts) {
});

this.rule("Hello1", {salience: 10}, [Message, "m", "m.name == 'Hello'"], function (facts) {
});

Or using the DSL

rule Hello4 {
    salience: 7;
    when {
        m: Message m.name == 'hello';
    }
    then {}
}

rule Hello3 {
    salience: 8;
    when {
        m: Message m.name == 'hello';
    }
    then {}
}

rule Hello2 {
    salience: 9;
    when {
        m: Message m.name == 'hello';
    }
    then {}
}

rule Hello1 {
    salience: 10;
    when {
        m: Message m.name == 'hello';
    }
    then {}
}

In the above flow we define four rules each with a different salience, when a single message is asserted they will fire in order of salience (highest to lowest).

var fired = [];
flow1
    .getSession(new Message("Hello"))
    .on("fire", function (name) {
        fired.push(name);
    })
    .match()
    .then(function(){
        console.log(fired); //["Hello1", "Hello2", "Hello3", "Hello4"]
    });

Scope

Scope allows you to access function from within your rules.

If you are using vanilla JS you can use the scope option when defining your rule.

this.rule("hello rule", {scope: {isEqualTo: isEqualTo}},
   [
      ["or",
         [String, "s", "isEqualTo(s, 'hello')"],
         [String, "s", "isEqualTo(s, 'world')"]
      ],
      [Count, "called", null]
   ],
   function (facts) {
      facts.called.called++;
   });

If you are using the dsl.

function matches(str, regex){
    return regex.test(str);
}

rule Hello {
    when {
        m : Message matches(m.text, /^hello\s*world)?$/);
    }
    then {
        modify(m, function(){
            this.text += " goodbye";
        })
    }
}

rule Goodbye {
    when {
        m : Message matches(m.text, /.*goodbye$/);
    }
    then {
    }
}

Or you can pass in a custom function using the scope option in compile.

rule Hello {
    when {
        m : Message doesMatch(m.text, /^hello\sworld$/);
    }
    then {
        modify(m, function(){
            this.text += " goodbye";
        })
    }
}

rule Goodbye {
    when {
        m : Message doesMatch(m.text, /.*goodbye$/);
    }
    then {
    }
}

Provided the doesMatch function in the scope option of compile.

function matches(str, regex) {
   return regex.test(str);
};
var flow = nools.compile(__dirname + "/rules/provided-scope.nools", {scope: {doesMatch: matches}});

Constraints

Constraints define what facts the rule should match. The constraint is a array of either a single constraint (i.e. Bootstrap rule) or an array of constraints(i.e. Calculate).

Programmatically

[
   //Type     alias  pattern           store sequence to s1
  [Fibonacci, "f1", "f1.value != -1", {sequence:"s1"}],
  [Fibonacci, "f2", "f2.value != -1 && f2.sequence == s1 + 1", {sequence:"s2"}],
  [Fibonacci, "f3", "f3.value == -1 && f3.sequence == s2 + 1"],
  [Result, "r"]
]

Using nools DSL

when {
    f1 : Fibonacci f1.value != -1 {sequence:s1};
    f2 : Fibonacci f2.value != -1 && f2.sequence == s1 + 1 {sequence:s2};
    f3 : Fibonacci f3.value == -1 && f3.sequence == s2 + 1;
    r  : Result;
}
  1. Type - is the Object type the rule should match. The available types are

    • String - "string", "String", String
    • Number - "number", "Number", Number
    • Boolean - "boolean", "Boolean", Boolean
    • Date - "date", "Date", Date
    • RegExp - "regexp", "RegExp", RegExp
    • Array - "array", "Array", [], Array
    • Object - "object", "Object", "hash", Object
    • Custom - any custom type that you define
  2. Alias - the name the object should be represented as.

  3. Pattern(optional) - The pattern that should evaluate to a boolean, the alias that was used should be used to reference the object in the pattern. Strings should be in single quotes, regular expressions are allowed. Any previously defined alias/reference can be used within the pattern. Available operators are.

    • Not supported: bitwize operators & | << >> >>> ^ ~
    • &&, AND, and
    • ||, OR, or
    • >, <, >=, <=, gt, lt, gte, lte
    • ==, ===, !=, !==, =~, !=~, eq, seq, neq, sneq, like, notLike
    • +, -, *, /, %
    • - (unary minus)
    • ^ (pow operator)
    • . (member operator)
    • in (check inclusion in an array)
    • notIn (check that something is not in an array)
    • Defined helper functions
      • now - the current date
      • Date(year?, month?, day?, hour?, minute?, second?, ms?) - creates a new Date object
      • lengthOf(arr, length) - checks the length of an array
      • isTrue(something) - check if something === true
      • isFalse(something) - check if something === false
      • isRegExp(something) - check if something is a RegExp
      • isArray(something) - check if something is an Array
      • isNumber(something) - check if something is an Number
      • isHash(something) - check if something is strictly an Object
      • isObject(something) - check if something is any type of Object
      • isDate(something) - check if something is a Date
      • isBoolean(something) - check if something is a Boolean
      • isString(something) - check if something is a String
      • isUndefined(something) - check if something is a undefined
      • isDefined(something) - check if something is Defined
      • isUndefinedOrNull(something) - check if something is a undefined or null
      • isPromiseLike(something) - check if something is a "promise" like (containing then, addCallback, addErrback)
      • isFunction(something) - check if something is a Function
      • isNull(something) - check if something is null
      • isNotNull(something) - check if something is not null
      • dateCmp(dt1, dt2) - compares two dates return 1, -1, or 0
      • (years|months|days|hours|minutes|seconds)(Ago|FromNow)(interval) - adds/subtracts the date unit from the current time
  4. Reference(optional) - An object where the keys are properties on the current object, and values are aliases to use. The alias may be used in succeeding patterns.

Custom Constraint

When declaring your rules progrmmatically you can also use a function as a constraint. The function will be called with an object containing each fact that has matched previous constraints.

var HelloFact = declare({
    instance: {
        value: true,
        constructor: function (value) {
            this.value = value;
        }
    }
});

var flow = nools.flow("custom contraint", function (flow) {
    flow.rule("hello rule", [HelloFact, "h", function (facts) {
        return facts.h.value === true;
    }], function (facts) {
        console.log(facts.h.value); //always true
    });
});

var session = flow.getSession();
session.assert(new HelloFact(false));
session.assert(new HelloFact(true));
session.match().then(function(){
    console.log("DONE");
});

Not Constraint

The not constraint allows you to check that particular fact does not exist.

[
    [Number, "n1"],
    ["not", Number, "n2", "n1 > n2"]
]

Or using the DSL.


when {
    n1: Number;
    not(n2: Number n1 > n2);
}

The previous example will check that for all numbers in the workingMemory there is not one that is greater than n1.

Or Constraint

The or constraint can be used to check for the existence of multiple facts.

[
    ["or",
        [String, "s", "s == 'hello'"],
        [String, "s", "s == 'world'"],
        [String, "s", "s == 'hello world'"]
    ]
]

Using the DSL.

when {
    or(
        s : String s == 'hello',
        s : String s == 'world',
        s : String s == 'hello world'
    );
}

The previous example will evaluate to true if you have a string in workingMemory that equals hello, world, or 'hello world.

Or with Not

The or constraint can be combined with a not constraint to allow for the checking of multiple not conditions without the implcit and.

var flow = nools.flow("or condition with not conditions", function (flow) {
        flow.rule("hello rule", [
                ["or",
                    ["not", Number, "n1", "n1 == 1"],
                    ["not", String, "s1", "s1 == 'hello'"],
                    ["not", Date, "d1", "d1.getDate() == now().getDate()"]
                ],
                [Count, "called", null]
            ], function (facts) {
                facts.called.called++;
            });
        });
});

or using the dsl.

rule MultiNotOrRule {
    when {
        or (
            not(n1: Number n1 == 1),
            not(s1: String s1 == 'hello'),
            not(d1: Date d1.getDate() == now().getDate())
        );
        c: Count;
    }
    then{
        c.called++;
    }
}

Note Using the or with a not will cause the activation to fire for each not condition that passes. In the above examples if none of the three facts existed then the rule would fire three times.

From Constraint

The from modifier allows for the checking of facts that are not necessarily in the workingMemory.

The from modifier can be used to access properties on a fact in workingMemory or you can use javascript expressions.

To access properties on a fact you can use the fact name and the property you wish to use as the source for the from source.

[
    [Person, "p"],
    [Address, "a", "a.zipcode == 88847", "from p.address"],
    [String, "first", "first == 'bob'", "from p.firstName"],
    [String, "last", "last == 'yukon'", "from p.lastName"]
]

Or using the DSL.

when {
    p: Person:
    a: Address a.zipcode == 88847 from p.address;
    first: String first == 'bob' from p.firstName;
    last: String last == 'yukon' from p.lastName;
}

The above example will used the address, firstName and lastName from the person fact.

You can also use the from modifier to check facts that create a graph.

For example assume the person object from above has friends that are also of type Person.

[
    [Person, "p"],
    [Person, "friend", "friend.firstName != p.firstName", "from p.friends"],
    [String, "first", "first =~ /^a/", "from friend.firstName"]
]

Or using the DSL.

when {
    p: Person;
    friend: Person friend.firstName != p.firstName from p.friends;
    first: String first =~ /^a/ from friend.firstName;
}

The above example will pull the friend fact from the friends array property on fact p, and first from the friend's firstName.

You could achieve the same thing using the following code if you assert all friends into working memory.

when {
    p: Person;
    friend: Person friend in p.friends && friend.firstName != p.firstName && p.firstName =~ /^a/;
}

To specify the from source as an expression you can do the following.

[
    [Number, "n1", "from [1,2,3,4,5]"]
]

Or using the dsl

{
    n1: Number from [1,2,3,4,5];
}

Using the above syntax you could use from to bootstrap data.

You can also use any function defined in the scope of the rule or flow

flow.rule("my rule", {
    scope: {
        myArr: function(){
            return [1,2,3,4,5];
        }
    },
    [Number, "n1", "from myArr()"],
    function(facts){
        this.assert(facts.n1);
    }
}

Or using the dsl and the scope option.

rule "my rule", {
    when {
        n1: Number from myArr();
    }
    then{
        assert(n1);
    }
}

Exists Constraint

exists is the logical inversion of a not constraint. It checks for the existence of a fact in memory.

NOTE If there are multiple facts that satisfy the constraint the rule will ONLY be fired once.

[
    ["exists", Number, "n1", "n1 > 1"]
]

Or using the DSL.


when {
    exists(n1: Number n1 > 1);
}

Assuming the above constraint. The following facts would cause the rule to fire once since there is a number that is greater than 1.

session.assert(1);
session.assert(2);
session.assert(3);
session.assert(4);
session.assert(5);

Action

The action is a function that should be fired when all patterns in the rule match. The action is called in the scope of the engine so you can use this to assert, modify, or retract facts. An object containing all facts and references created by the alpha nodes is passed in as the first argument to the action.

So calculate's action modifies f3 by adding the value of f1 and f2 together and modifies f3 and retracts f1.

function (facts) {
        var f3 = facts.f3, f1 = facts.f1, f2 = facts.f2;
        var v = f3.value = f1.value + facts.f2.value;
        facts.r.result = v;
        this.modify(f3);
        this.retract(f1);
    }

The session is also passed in as a second argument so alternatively you could do the following.

function (facts, session) {
        var f3 = facts.f3, f1 = facts.f1, f2 = facts.f2;
        var v = f3.value = f1.value + facts.f2.value;
        facts.r.result = v;
        session.modify(f3);
        session.retract(f1);
    }

To define the actions with the nools DSL

then {
    modify(f3, function(){
        this.value = f1.value + f2.value;
    });
    retract(f1);
}

For rules defined using the rules language nools will automatically determine what parameters need to be passed in based on what is referenced in the action.

Async Actions

If your action is async you can use the third argument which should be called when the action is completed.

function (facts, engine, next) {
        //some async action
        process.nextTick(function(){
            var f3 = facts.f3, f1 = facts.f1, f2 = facts.f2;
            var v = f3.value = f1.value + facts.f2.value;
            facts.r.result = v;
            engine.modify(f3);
            engine.retract(f1);
            next();
        });
    }

If an error occurs you can pass the error as the first argument to next.

then{
   saveToDatabase(user, function(err){
      next(new Error("Something went BOOM!"));
   });
}

If you are using a Promises/A+ compliant library you can just return a promise from your action and nools will wait for the promise to resolve before continuing.

then{
   return saveToDatabase(user); // assume saveToDatabase returns a promise
}

Globals

Globals are accessible through the current working scope of rules defined in a dsl, very similar to using the scope option when compiling.

Note globals are not part of the working memory and therefore are not accessible in the LHS (when) or your rule.

Globals are used like the following:

global PI = Math.PI;
global SOME_STRING = 'some string';
global TRUE = true;
global NUM = 1.23;
global DATE = new Date();

rule "A Rule" {
    when {
    	$obj: Object;
    }
    then{
    	console.log(PI); //Math.PI;
    	console.log(SOME_STRING); //"some string"
    	console.log(TRUE); //true
    	console.log(NUM); //1.23
    	console.log(DATE); //Thu May 23 2013 15:49:22 GMT-0500 (CDT)
    }
}

If you are using nools in node you can also use a require statement.

NOTE require does not currently work for relative paths.

global util = require("util");

rule "A Rule" {
    when {
    	$obj: Object;
    }
    then{
    	util.log("HELLO WORLD");
    }
}

Importing

The import statement allows you to import other nools files into the current one. This can be used to split up logical flows into small reusable groups of rules.

Define our common model to be used across our flows.

//define.nools
define Count{
    constructor: function(){
        this.called = 0;
    }
}

Create a rules file which imports the define.nools to define our Count model.

//orRule.nools

//import define.nools
import("./define.nools");
rule orRule {
    when {
        or(
            s : String s == 'hello',
            s : String s == 'world'
        );
        count : Count;
    }
    then {
        count.called++;
        count.s = s;
    }
}

Same as orRule.nools import our define.nools

//notRule.nools
import("./defines.nools");
rule notRule {
    when {
        not(s : String s == 'hello');
        count : Count
    }
    then {
        count.called++;
    }
}

Now we can use orRule.nools and notRule.nools to compose a new flow that contains define.nools, orRule.nools and notRule.nools.

Note nools will handle duplicate imports, in this case define.nools will only be imported once.

//import
import("./orRules.nools");
import("./notRules.nools");

Emitting custom events.

You may also emit events from your rule actions using the sessions emit function.

then {
    modify(f3, function(){
        this.value = f1.value + f2.value;
    });
    retract(f1);
    emit("my custom event");
}

To listen to the event just use the on method of the session.

var session = flow.getSession();

session.on("my custom event", function(){
    //custom event called.
});

Browser Support

Nools can also be used in the browser. The only difference is that you cannot pass a file location to the compile method instead you must provide the source.

Nools is compatible with amd(requirejs) and can also be used in a standard script tag.

Example 1.

In this example we compile rules definitions inlined in a script tag.

<script type="text/javascript" src="nools.js"></script>
<script type="text/nools" id="simple">
define Message {
    message : "",
    constructor : function (message) {
        this.text = message;
    }
}

rule Hello {
    when {
        m : Message m.text =~ /^hello\sworld$/
    }
    then {
        modify(m, function(){
            this.text += " goodbye";
        });
    }
}

rule Goodbye {
    when {
        m : Message m.text =~ /.*goodbye$/
    }
    then {
        document.getElementById("output").innerHTML += m.text + "</br>";
    }
}
</script>
<script type="text/javascript">
    function init() {
       //get the source
       var source = document.getElementById("simple").innerHTML;
       //compile the source. The name option is required if compiling directly.
       var flow = nools.compile(source, {name: "simple"}),
                Message = flow.getDefined("message"),
                session = flow.getSession();
        //assert your different messages
        session.assert(new Message("goodbye"));
        session.assert(new Message("hello world"));
        session.match();
    }
</script>

Using a compiled dsl.

You may also use the nools executable to compile source into a browser friendly format skipping the need for compiling each time.

nools compile ./my/rules.nools > ./compiled.js

To use the flow require the compile version either through a script tag, amd/requirejs, or commonjs require.

If you import the flow using a script tag you can get a reference to the flow by using nools.getFlow.

nools.getFlow("rules");

You may also specify the name of the flow when compiling, it defaults to the name of the nools file less ".nools"

nools compile -n "my rules" ./my/rules.nools
nools.getFlow("my rules");

If you are using requirejs or nools must be required using something other than require("nools") then you can specify a location of the nools source.

nools compile -nl "./location/to/nools" ./my/rules.nools

RequireJS examples

Examples of using nools with require js are located in the examples directory.

Examples

Fibonacci

"use strict";

var nools = require("nools");

var Fibonacci = function (sequence, value) {
    this.sequence = sequence;
    this.value = value || -1;
};

var Result = function (result) {
    this.result = result || -1;
};


var flow = nools.flow("Fibonacci Flow", function (flow) {

    flow.rule("Recurse", [
        ["not", Fibonacci, "f", "f.sequence == 1"],
        [Fibonacci, "f1", "f1.sequence != 1"]
    ], function (facts) {
        var f2 = new Fibonacci(facts.f1.sequence - 1);
        this.assert(f2);
    });

    flow.rule("Bootstrap", [
          Fibonacci, "f", "f.value == -1 && (f.sequence == 1 || f.sequence == 2)"
    ], function (facts) {
        var f = facts.f;
        f.value = 1;
        this.modify(f);
    });

    flow.rule("Calculate", [
        [Fibonacci, "f1", "f1.value != -1", {sequence:"s1"}],
        [Fibonacci, "f2", "f2.value != -1 && f2.sequence == s1 + 1", {sequence:"s2"}],
        [Fibonacci, "f3", "f3.value == -1 && f3.sequence == s2 + 1"],
        [Result, "r"]
    ], function (facts) {
        var f3 = facts.f3, f1 = facts.f1, f2 = facts.f2;
        var v = f3.value = f1.value + facts.f2.value;
        facts.r.result = v;
        this.modify(f3);
        this.retract(f1);
    });
});

var r1 = new Result(),
    session1 = flow.getSession(new Fibonacci(10), r1),
    s1 = new Date;
session1.match().then(function () {
    console.log("%d [%dms]", r1.result, new Date - s1);
    session1.dispose();
});

var r2 = new Result(),
    session2 = flow.getSession(new Fibonacci(150), r2),
    s2 = new Date;
session2.match().then(function () {
    console.log("%d [%dms]", r2.result, new Date - s2);
    session2.dispose();
});

var r3 = new Result(),
    session3 = flow.getSession(new Fibonacci(1000), r3),
    s3 = new Date;
session3.match().then(function () {
    console.log("%d [%dms]", r3.result, new Date - s3);
    session3.dispose();
});

Output

55 [43ms]
9.969216677189305e+30 [383ms]
4.346655768693743e+208 [3580ms]

Fibonacci with nools DSL

//Define our object classes, you can
//also declare these outside of the nools
//file by passing them into the compile method
define Fibonacci {
    value:-1,
    sequence:null
}
define Result {
    value : -1
}

rule Recurse {
    when {
        //you can use not or or methods in here
        not(f : Fibonacci f.sequence == 1);
        //f1 is how you can reference the fact else where
        f1 : Fibonacci f1.sequence != 1;
    }
    then {
        assert(new Fibonacci({sequence : f1.sequence - 1}));
    }
}

rule Bootstrap {
   when {
       f : Fibonacci f.value == -1 && (f.sequence == 1 || f.sequence == 2);
   }
   then{
       modify(f, function(){
           this.value = 1;
       });
   }
}

rule Calculate {
    when {
        f1 : Fibonacci f1.value != -1 {sequence : s1};
        //here we define constraints along with a hash so you can reference sequence
        //as s2 else where
        f2 : Fibonacci f2.value != -1 && f2.sequence == s1 + 1 {sequence:s2};
        f3 : Fibonacci f3.value == -1 && f3.sequence == s2 + 1;
        r : Result
    }
    then {
        modify(f3, function(){
            this.value = r.result = f1.value + f2.value;
        });
        retract(f1);
    }
}

And to run

var flow = nools.compile(__dirname + "/fibonacci.nools");

var Fibonacci = flow.getDefined("fibonacci"), Result = flow.getDefined("result");
var r1 = new Result(),
    session1 = flow.getSession(new Fibonacci({sequence:10}), r1),
    s1 = +(new Date());
session1.match().then(function () {
    console.log("%d [%dms]", r1.result, +(new Date()) - s1);
    session1.dispose();
});

var r2 = new Result(),
    session2 = flow.getSession(new Fibonacci({sequence:150}), r2),
    s2 = +(new Date());
session2.match().then(function () {
    console.log("%d [%dms]", r2.result, +(new Date()) - s2);
    session2.dispose();
});

var r3 = new Result(),
    session3 = flow.getSession(new Fibonacci({sequence:1000}), r3),
    s3 = +(new Date());
session3.match().then(function () {
    console.log("%d [%dms]", r3.result, +(new Date()) - s3);
    session3.dispose();
});

License

MIT https://github.com/C2FO/nools/raw/master/LICENSE

Meta

  • Code: git clone git://github.com/C2FO/nools.git

nools's People

Contributors

bryant1410 avatar devside avatar doug-martin avatar dustinsmith1024 avatar geoff-lee-lendesk avatar gitter-badger avatar kidrane avatar larryprice avatar paullryan avatar scdf avatar tommy20061222 avatar vikeen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

nools's Issues

SEND + MORE = MONEY

Tried to solve this classic problem just for fun in NOOLS. Goes off and never returns and Chrome says it died. Based it on a CLIPS version found here:

http://commentsarelies.blogspot.com/2006/11/send-more-money-in-clips.html

I formulated this NOOLS file below and "asserted" all the Digits (0..9). Any ideas?
Thought it might be a good baseline test.

rule SendMoreMoney {
when {
s : Digit s.value != 0 { value : s1 };
e : Digit e.value != s1 { value: e1};
n : Digit n.value != s1 && n.value != e1 { value : n1 };
d : Digit d.value != s1 && d.value != e1 && d.value != n1 { value : d1 };
m : Digit m.value != s1 && m.value != e1 && m.value != n1 && m.value != d1 && m.value != 0 { value : m1 };
o : Digit o.value != s1 && o.value != e1 && o.value != n1 && o.value != d1 && o.value != m1 { value : o1 };
r : Digit r.value != s1 && r.value != e1 && r.value != n1 && r.value != d1 && r.value != m1 && r.value != o1 { value : r1 };
y : Digit y.value != s1 && y.value != e1 && y.value != n1 && y.value != d1 && y.value != m1 && y.value != o1 && y.value != r1 { value : y1 };
test : Digit s1_1000 + e1_100 + n1_10 + d1
+ m1_1000 + o1_100 + r1_10 + e1 ==
m1_10000 + o1_1000 + n1_100 + e1_10 + y1;

}
then {
    emit('pathDone', {sout : s, eout : e, nout : n, dout : d, mout : m, oout: o, rout : r, yout : y});
}

}

Compiling or() in DSL 'when' clause emits bad JS

I have a rule expressed in the DSL as follows:

rule beta {
    when {
        or(
           v1 : Value v1.id == 'hello',
           v2 : Value v2.id == 'goodbye'
           );
        r : Result
    }
    then {
        r.v.push("result"));
    }
}

Compiling (w. v0.1.8) the above yields the following JS (relevant excerpt):

                    }, ["or", , [Value, "v1", "v1.id == 'hello'"],
                        [Value, "v2", "v2.id == 'goodbye'"],
                        [Result, "r"]
                    ], function(facts, flow) {

...which causes a runtime error.

I'd expect the DSL rule to compile down to something more like:

                    }, ["or", [ 
                                  [Value, "v1", "v1.id == 'hello'"],
                                  [Value, "v2", "v2.id == 'goodbye'"]
                                ],
                        [Result, "r"]
                    ], function(facts, flow) {

...which works fine at runtime.

Is my expectation correct?

Equality operator not working as expected

I have an object defined thus:

define Value {
    id : null,
    v : null,
    constructor : function (id, value) {
        this.id = id;
        this.v = value;
     }
}

I'm asserting a fact like this:

s.assert(new Value("xyz", 27));

Then looking for it in a when clause like this:

v4 : Value v4.id =~ /xyz/ && v4.v == 27;

The == comparison fails to trigger the rule. The following does trigger the rule:

 v4 : Value v4.id =~ /xyz/ && v4.v >= 27 && v4.v <= 27;

I expected the == to work.

Recommended way to profile rules execution?

I'm trying to track down a significant slow-down in my rules execution. I'm fairly new to NodeJS's profiling tools, so this may be operator error. Here's what I'm doing...

export D8_PATH=/usr/local/bin
node --prof --log_all --log_snapshot_positions script.js
/usr/local/bin/tools/mac-tick-processor >> v8-processed.log

Strangely, it seems that most of the total ticks go unaccounted...

Notice that there are 1486 ticks, but there are just not that may reported in the bottom-up profile. I'm wondering if this has to do with the use of eval in compiling rules...

Statistical profiling result from v8.log, (1486 ticks, 12 unaccounted, 0 excluded).

 [Unknown]:
   ticks  total  nonlib   name
     12    0.8%

 [Shared libraries]:
   ticks  total  nonlib   name

 [JavaScript]:
   ticks  total  nonlib   name
      9    0.6%    0.6%  Function: wrapper /Volumes/Macintosh HD2/git/.../node_modules/nools/node_modules/declare.js/declare.js:617
      8    0.5%    0.5%  KeyedLoadIC: A keyed load IC from the snapshot
      6    0.4%    0.4%  LazyCompile: *EventEmitter.emit events.js:54
...

 [C++]:
   ticks  total  nonlib   name
    219   14.7%   14.7%  _chmod
     99    6.7%    6.7%  node::WrappedScript::CompileRunInThisContext
     37    2.5%    2.5%  ___vfprintf
...

 [GC]:
   ticks  total  nonlib   name
     74    5.0%

 [Bottom up (heavy) profile]:
  Note: percentage shows a share of a particular caller in the total
  amount of its parent calls.
  Callers occupying less than 2.0% are not shown.

   ticks parent  name
    219   14.7%  _chmod
     33   15.1%    LazyCompile: *fs.statSync fs.js:523
     33  100.0%      Function: statPath module.js:88
     29   87.9%        LazyCompile: *tryExtensions module.js:148
     29  100.0%          Function: ~Module._findPath module.js:160
     26   89.7%            Function: ~Module._resolveFilename module.js:323
      3   10.3%            LazyCompile: *Module._resolveFilename module.js:323
      4   12.1%        LazyCompile: *tryFile module.js:138
      4  100.0%          Function: ~Module._findPath module.js:160
      4  100.0%            Function: ~Module._resolveFilename module.js:323
     20    9.1%    Function: ~fs.statSync fs.js:523
     20  100.0%      Function: statPath module.js:88
      9   45.0%        LazyCompile: *tryExtensions module.js:148
      9  100.0%          Function: ~Module._findPath module.js:160
      9  100.0%            Function: ~Module._resolveFilename module.js:323
      7   35.0%        LazyCompile: *tryFile module.js:138
      5   71.4%          Function: ~tryExtensions module.js:148
      5  100.0%            Function: ~Module._findPath module.js:160
      2   28.6%          Function: ~Module._findPath module.js:160
      2  100.0%            Function: ~Module._resolveFilename module.js:323
      4   20.0%        Function: ~tryFile module.js:138
      4  100.0%          Function: ~tryExtensions module.js:148
      4  100.0%            Function: ~Module._findPath module.js:160
     16    7.3%    Function: ~defineProps /Volumes/Macintosh HD2/git/rebit/requalify3/server/node_modules/nools/node_modules/declare.js/declare.js:695
...

Conlcusion

_Is there a better way to see where the hot spots are in my rules execution?_

Nools compile: Unexpected token name «start», expected punc «,»

I have no clue what is wrong with this rule:

rule "some rule" {
  when {
    $q: String $q == "start";
  }
  then {
  }
}

When I run nools compile I get this error:

Unexpected token name «start», expected punc «,»
Error
    at new JS_Parse_Error (/usr/lib/node_modules/uglify-js/lib/parse.js:196:18)
    at js_error (/usr/lib/node_modules/uglify-js/lib/parse.js:204:11)
    at croak (/usr/lib/node_modules/uglify-js/lib/parse.js:636:9)
    at token_error (/usr/lib/node_modules/uglify-js/lib/parse.js:644:9)
    at expect_token (/usr/lib/node_modules/uglify-js/lib/parse.js:657:9)
    at expect (/usr/lib/node_modules/uglify-js/lib/parse.js:660:36)
    at expr_list (/usr/lib/node_modules/uglify-js/lib/parse.js:1137:44)
    at /usr/lib/node_modules/uglify-js/lib/parse.js:1152:23
    at /usr/lib/node_modules/uglify-js/lib/parse.js:683:24
    at expr_atom (/usr/lib/node_modules/uglify-js/lib/parse.js:1115:35)

Clarify the purpose of next()

I'm guessing next() needs to be called when asserting from a callback within a then { } clause but I'm not sure because as far as I can tell next() is not clearly documented apart from:

If any arguments are passed into next it is assumed there was an error and the session will error out.

Example:

then {
  io.sockets.on('connection', function(socket) {
    assert(new Thing(socket));
    next();
  });
}

Question: Why don't rules with the same constraint don't all match?

I have a rule file below based on one of your examples. Originally I started out playing with salience (took me a short while to figure out the syntax in the DSL to set the salience since their weren't an examples). Anyway, in the example below the "count3" rule does not fire ever. I can if I start setting salience, but, even then it seems nonsensical at times. The "count2" rule that modifies the Counter seems to win always. I would assume that all the rules would fire. Thanks.

define Counter {
count: 0,
constructor: function(count){
this.count = count;
}
}

rule "counted too high" {
when {
ctr: Counter ctr.count == 100;
}
then{
console.log("Look ma! I counted to " + ctr.count);
halt();
}
}

rule "not count" {
when {
not(ctr: Counter);
}
then{
console.log("Imma gonna count!");
assert(new Counter(1));
}
}

rule count1 {
when{
ctr: Counter {count: count}
}
then{
console.log("Count 1");
}
}

rule count2 {

when{
    ctr: Counter {count: count}
}
then{
    modify(ctr, function(){this.count = count + 1;});
    console.log("Count 2");
}

}

rule count3 {

when{
    ctr: Counter {count: count}
}
then{
        console.log("Count 3");    
}

}

comb 0.2.0 breaks nools

Comb is set in package.json to be >=0.1.7 this breaks the build

To fix this you need to either revert the build to 0.1.7 or update the code, what do you suggest? can we have this fixed ASAP?

Functions calls used in constraint predicates

The rule engine has trouble parsing rules with constructs like:

when {
        p : Property p.name != null && p.value != -1 {name:pn,value:pv};
        d : Dimension d.name!=null && isTrue(d.range.contains(pv));
    }

but this works:

when {
        d : Dimension d.name!=null && isTrue(d.range.contains(20));
}

Here range is a class "Range" that encapsulates the "contains" function that determines if the value is in range.

The pv doesn't seem to be considered a valid symbol by the parser when parsing the "contains(pv)" function call and returns the error that "pv is not defined" (I have tried p.value here also). Pulling off the "isTrue" parses, but the rule no longer functions correctly (it always fires).

Simple models without functions work as seen in the following:

when {
     p : Property p.name != null && p.value != -1;
     d : Dimension2 d.name!=null && p.value >= d.low && d.high >= p.value;
}

Is there a fix for this? Allowing function calls here is a step up in the expressive range of the language.

Full Code below:

the dsl: /rules/routebroken.nools

define Dimension {
    name:null,
    range: new Range(0,0),

    constructor : function(n,r){
        this.name = n;
        this.range = r;
    }
}

define Property {
    name:null,
    value:-1,

    constructor : function(n,v){
        this.name = n;
        this.value = v;
    }
}

rule RouteBroken {
    priority:1,
    when {
        p : Property p.name != null && p.value != -1 {name:pn,value:pv};
        d : Dimension d.name!=null && isTrue(d.range.contains(pv));
    }
    then {
        console.log("Route broken:  Dimension:"+d.name+"=>"+d.range.getRange()+" contains("+p.value+")");
    }
}

the program: routedslbroken.js

if (!(typeof exports === "undefined")) {
    var nools = require("nools");

    var range_module = require("./modules/range/range.class.js");
    var enum_module = require("./modules/enum/enum.js");

    var Range = range_module.Range;
    var Enum = enum_module.Enum;
}

var flow = nools.compile(__dirname + "/rules/routebroken.nools");
Dimension = flow.getDefined("Dimension");
Property = flow.getDefined("Property");

r = new Range(4,200);
d = new Dimension("Quantity",r);

var session = flow.getSession();

session.assert(new Dimension("Quantity",r));
session.assert(new Property("Quantity",10));
session.assert(new Property("Quantity",100));
session.assert(new Property("Quantity",1000)); // should not fire a rule.

session.match().then(
    function(){
        console.log("Done");
    },
    function(err){
        //uh oh an error occurred
        console.error(err);
    });

/modules/enum/enum.js

(function (exports) {
    function copyOwnFrom(target, source) {
        Object.getOwnPropertyNames(source).forEach(function(propName) {
            Object.defineProperty(target, propName,
                Object.getOwnPropertyDescriptor(source, propName));
        });
        return target;
    }

    function Symbol(name, props) {
        this.name = name;
        if (props) {
            copyOwnFrom(this, props);
        }
        Object.freeze(this);
    }
    /** We don’t want the mutable Object.prototype in the prototype chain */
    Symbol.prototype = Object.create(null);
    Symbol.prototype.constructor = Symbol;
    /**
     * Without Object.prototype in the prototype chain, we need toString()
     * in order to display symbols.
     */
    Symbol.prototype.toString = function () {
        return "|"+this.name+"|";
    };
    Object.freeze(Symbol.prototype);

    Enum = function (obj) {
        if (arguments.length === 1 && obj !== null && typeof obj === "object") {
            Object.keys(obj).forEach(function (name) {
                this[name] = new Symbol(name, obj[name]);
            }, this);
        } else {
            Array.prototype.forEach.call(arguments, function (name) {
                this[name] = new Symbol(name);
            }, this);
        }
        Object.freeze(this);
    }
    Enum.prototype.symbols = function() {
        return Object.keys(this).map(
            function(key) {
                return this[key];
            }, this
        );
    }
    Enum.prototype.contains = function(sym) {
        if (! sym instanceof Symbol) return false;
        return this[sym.name] === sym;
    }
    exports.Enum = Enum;
    exports.Symbol = Symbol;
}(typeof exports === "undefined" ? this.enums = {} : exports));
// Explanation of this pattern: http://www.2ality.com/2011/08/universal-modules.html

/modules/range/range.class.js

(function (exports) {

    Range = function (start_, end_, step_) {
        var range;
        var typeofrange;
        var typeofStart;
        var typeofEnd;
        var largerange = 100;
        var rangelow,rangehigh;

        Array.prototype.contains = function(k) {
            for(var p in this)
                if(this[p] === k)
                    return true;
            return false;
        }

        var init = function(start, end, step) {
            range = [];
            typeofStart = typeof start;
            typeofEnd = typeof end;

            if (typeof(start) == "object")
            {
                if (start instanceof Array) {
                    range = start;
                    typeofrange = "array";
                }
                // TODO: Hash?
                else { // assume an enum if it isn't an array.
                    range = start;
                    typeofrange = "enum";
                }

                return;
            }

            if (step === 0) {
                throw TypeError("Step cannot be zero.");
            }

            if (typeofStart == "undefined" || typeofEnd == "undefined") {
                throw TypeError("Must pass start and end arguments.");
            } else if (typeofStart != typeofEnd) {
                throw TypeError("Start and end arguments must be of same type.");
            }

            typeof step == "undefined" && (step = 1);

            if (end < start) {
                step = -step;
            }

            rangelow=start;
            rangehigh=end;
            if (typeofStart == "number") {
                if ((end-start)/step >= largerange || step == 1) {
                    typeofrange = "range";
                    if (step != 1){
                        throw TypeError("Step size must be 1 for ranges larger than "+largerange+".");
                    }
                }
                else {
                    typeofrange = typeofStart;
                    while (step > 0 ? end >= start : end <= start) {
                        range.push(start);
                        start += step;
                    }

                }

            } else if (typeofStart == "string") {

                typeofrange = typeofStart;
                if (start.length != 1 || end.length != 1) {
                    throw TypeError("Only strings with one character are supported.");
                }

                start = start.charCodeAt(0);
                end = end.charCodeAt(0);

                while (step > 0 ? end >= start : end <= start) {
                    range.push(String.fromCharCode(start));
                    start += step;
                }

            } else {
                throw TypeError("Only string and number types are supported");
            }
        };
        var getRange = function() { if (typeofrange=="range") return getLow()+".."+getHigh();
            else return range; };
        var getLow = function () { return rangelow; };
        var getHigh = function () { return rangehigh; };

        var contains = function (value) {
            if (typeofrange == "range") {
                return value >= rangelow && value <= rangehigh;
            }
            return range.contains(value)
            /*if (typeofrange == "enum") {
                return range.contains(value)
            }
            for(var p in range)
                if(this[p] === value)
                    return true;
            return false;

            */
        }

        init(start_, end_, step_);

        return {
            getRange:getRange,
            getLow:getLow,
            getHigh:getHigh,
            contains:contains
        };

    };

    exports.Range = Range;
}(typeof exports === "undefined" ? this.range = {} : exports));
//exports.Range = Range;

Fact matching more than once for a rule

define Thing {
  constructor: function(value) {
    this.value = value;
  }
}

rule "some rule" {
   when {
     $1: Thing $1.value == "something";
     $2: Thing;
   }
}

The fact matched by $1 will also be matched by $2, that doesn't feel right in my opinion. Matching typically happens left-to-right, therefore $1 should have priority over $2.

Facts defined in compile options not available via getDefined()

I think I should be able to do this:

custom-facts.js

module.exports = {
  CustomClass: function() {...}
}

custom-flow.js

var facts = require('custom-facts');

var flow = nools.compile('rules.nools', {
  defined: facts
});

custom = flow.getDefined('CustomClass');

But it seems that, event though the rules compile correctly, the classes are not available via getDefined(). It would be nice to have this work. I'd like to define my classes in coffeescript (in an separate file) and provide them to the rest of the app as if they were part of the rules file.

Or am I approaching this wrong?

Recompiling the same nools file throws an error

Use case: I'm using nools in NodeJS. The rules are in a ".nools" file and can be changed at any time. I need to be able to recompile the same file without specifying a new name.

I saw in lib/index.js that you're using the flows object to keep track of compiled files and you're checking in the FlowContainer's constructor is flows[name] already exists (this is where the error is thrown).

Ideally, we would have a way to remove items from this 'flows' object which btw will grow big if your application is compiling lots of '.nools' files. I know I can recompile fine if I use another name in the options parameter, but I don't think it's the appropriate way of doing things.

How to write a constraint that calls a custom function?

Just trying to understand the constraints model in nools.

I like to call any functions as part of constraint evaluation. Why is this not possible at the moment in nools. Why are only predefined functions and only property evaluations allowed?

more than two conditions in an or() block

I am looking for a way to have more than two conditions in an or() block.
The following rule will not parse:

rule hello {
when {
or(
a : String a == 'hello',
b : String b == 'hello',
c : String c == 'hello'
);
}
then {
}
}

A possible alternative notation might be something like:

rule hello {
when {
a : String b : String c : String a == 'hello' || b == 'hello' || c == 'hello'
}
then {
}
}

Logical AND not working in constraints

Example ($q.something is undefined):

$q: Thing !isUndefined($q.something) && $q.something.isTrue;

This will result in an error:

TypeError: Cannot read property 'isTrue' of undefined

If && would work as a logical AND the second term would not be evaluated.

Nools, node 0.10.0 support

It would be great to be able to use nools withe new version of node (much more performant version of node) however it currently breaks based on some deprecations the first encountered deprecation is a follows:

Currently nools breaks on when on node 0.10.0 because of the use of processTick.

Error: (node) warning: Recursive process.nextTick detected. This will break in the next version of node. Please use setImmediate for recursive deferral.
    at maxTickWarn (node.js:375:15)
    at process.nextTick (node.js:480:9)
    at wrapper.<anonymous> (.../node_modules/nools/lib/index.js:286:29)
    at .../node_modules/nools/node_modules/function-extended/index.js:43:39
    at fire (.../node_modules/nools/lib/index.js:258:21)
    at process._tickCallback (node.js:415:13)

Deleting previously defined flows

I can't seem to find a way to delete a flow i previously defined in nools. I need this as my rule engine will be running for a long duration and may load several rule sets over time.

What's the best way to do this ?

Unable to load shared library ... node-proxy.node

I can't seem to require('nools') without getting this error:

Error: Unable to load shared library /private/tmp/node_modules/nools/node_modules/comb/node_modules/node-proxy/lib/node-proxy.node
    at Object..node (module.js:472:11)
    at Module.load (module.js:348:31)
    at Function._load (module.js:308:12)
    at Module.require (module.js:354:17)
    at require (module.js:370:17)
    at Object.<anonymous> (/private/tmp/node_modules/nools/node_modules/comb/lib/base/proxy.js:1:75)
    at Module._compile (module.js:441:26)
    at Object..js (module.js:459:10)
    at Module.load (module.js:348:31)
    at Function._load (module.js:308:12)

I'm using Mac OS 10.6.

Constraint with $t[$t.step] fails compilation

Contraint:

$t: Thing ($t.step && isUndefined($t[$t.step]))

fails with this error:

Error: Invalid expression '($t.step && isUndefined($t[$t.step]))'
    at Object.exports.parseConstraint (/home/steven/bin/.nmgm/nools/node_modules/nools/lib/parser/index.js:10:19)
    at constraintsToJs (/home/steven/bin/.nmgm/nools/node_modules/nools/lib/compile/transpile.js:82:61)
    at /home/steven/bin/.nmgm/nools/node_modules/nools/lib/compile/transpile.js:136:24
    at Array.map (native)
    at wrapper.map (/home/steven/bin/.nmgm/nools/node_modules/nools/node_modules/array-extended/index.js:222:28)
    at wrapper.extendedMethod [as map] (/home/steven/bin/.nmgm/nools/node_modules/nools/node_modules/extended/node_modules/extender/extender.js:400:40)
    at /home/steven/bin/.nmgm/nools/node_modules/nools/lib/compile/transpile.js:135:45
    at Array.map (native)
    at wrapper.map (/home/steven/bin/.nmgm/nools/node_modules/nools/node_modules/array-extended/index.js:222:28)
    at wrapper.extendedMethod [as map] (/home/steven/bin/.nmgm/nools/node_modules/nools/node_modules/extended/node_modules/extender/extender.js:400:40)

If I replace isUndefined($t[$t.step]) with isUndefined($t) compilation works.

Nools client-side!

Hi Doug,

I would like to use your excellent rules engine 'nools' in a single page browser app client-side.

I have a requirement for an 'occasionally (dis)connected app' which would benefit from having a rules engine run in the browser. This would mean I could use the same rules both in NodeJS and client-side... this would be awesome!

What would be required to have nools less dependent on node or at least available client side!?

Regards
Clinton

array reference gives invalid expression error

Example of a simple rule below.
The console.log of the _domainValues[0] & [1] report correct values.
However when used in the constraint expression, the rules compiler reports
"Invalid expression". Any thoughts?

rule CheckAndAssertActualWeight {
    when {
            actualWeight_domain: ActualWeight_Domain {values: _domainValues};
            actualWeight_EnteredValue: ActualWeight_EnteredValue 
                (
                    actualWeight_EnteredValue.value >= _domainValues[0] &&
                    actualWeight_EnteredValue.value <= _domainValues[1]
                ) {value : _entered};
    }
    then {
        assert( new ActualWeight_Value({value:_entered}) );
        console.log("ActualWeight value assigned: %s", _entered);   
        console.log("ActualWeight domain %s %s.", _domainValues[0], _domainValues[1]);  
    }
}

Performance degradation in [email protected]

Hi,

We were using server side [email protected] for a while and recently upgraded to version 0.1.0.

We've seen almost a factor 2 degradation in performance (our test suite goes from 30-40 seconds to more than 1 minute).

We are very sensitive on performance as we are using big calculation models and it directly affects end user's response time.

I looked at both the current code and the changes introduced in 0.1.0 without a real clue about what is causing this performance degradation.

Would you be open to have a discussion on what may have caused this degradation and if there is a way we can help in improving performance ?

Two string comparisons in same expression fails

I have an object defined thus:

define Value {
    id : null,
    v : null,
    constructor : function (id, value) {
        this.id = id;
        this.v = value;
     }
}

I'm asserting a fact like this:

s.assert(new Value("xyz", "abc"));

Then looking for it in a when clause like this:

v4 : Value v4.id =~ /xyz/ && v4.v =~ /abc/;

The above fails to trigger the rule. The following does trigger the rule:

 v4 : Value v4.id =~ /xyz/ && v4.v ==  'abc';

For some reason, two string comparisons in the same rule don't want to play nice.

How can I use 'external' facts

Say I have a fact and a rule that checks on that fact but I also wnat to check on e.g. the time of day. How would I do that?

My current config is:

define Event {
    uuid: '',
    property: '',
    value: undefined,
    constructor : function(event){
        this.uuid = event.uuid,
        this.property = event.property,
        this.value = event.value;
    }
}

rule PowerConsumption {
    when {
        e1: Event e1.uuid == 'ddee00df-399d-4a81-898e-60d7d4185cf5' &&
                  e1.property == 'actual_consumed_power' &&
                  e1.value > 1;
    }
    then {
        console.log('Matched');
    }
}

This rule checks whether the actual consumed power reported by a sensor with the specified uuid exceeds 1kW. However, I like to add constraints not directly related to that specific event, e.g. the time of day or the status of another sensor that not necessarily changed status. How would I do that?

Option to not cache flows or export the flow cache in index.js

I use nools to create a large number of flows dynamically from a set of flow definitions. Each flow definition is stored by a unique name which i use to create the flow object.

These flow objects are then managed by a LRU cache which disposes the flow object when required.

However in certain scenarios the LRU cache is destroyed and dispose is not called. In this case I have no more references available of all my previously created flows and when i try to recreate them I get an error saying they already exist.

I can bypass this by asking nools if the flow exists before recreating it.

However what do I do in the scenario when I never recreate a flow again. Wouldn't there be a memory leak caused due the reference maintained on the flows cache in index.js ?

An example to show more clearly what i mean

  • I create cache 1
  • I create Flow A and store in cache 1
  • I create Flow B and store in cache 1
  • I delete cache 1
  • I create cache 2
  • I create Flow A and find it already exists so i use it and store it in cache 2
  • I create Flow C and store it in cache 2

In this case if i never try to create Flow B again it will forever remain in the flows object and never be removed from memory.

I think there should be an option to disable storing a created flow in the flows cache in index.js or else this should be exported so that it can be cleared manually when needed.

v0.1.10 breaks Node's require() function

I get this exception:

/home/steven/dev/xxxxxxxxxxx/node_modules/nools/lib/compile/common.js:26
        throw new Error("Invalid action : " + body + "\n" + e.message);
              ^
Error: Invalid action : ((function(){var io= scope.io;
    return require('/home/steven/dev/xxxxxxxxxxx/src/node/socket.io')
})())
Cannot find module '/home/steven/dev/xxxxxxxxxxx/src/node/socket.io'
    at createFunction (/home/steven/dev/xxxxxxxxxxx/node_modules/nools/lib/compile/common.js:26:15)
    at /home/steven/dev/xxxxxxxxxxx/node_modules/nools/lib/compile/index.js:172:25
    at Array.forEach (native)
    at forEach (/home/steven/dev/xxxxxxxxxxx/node_modules/nools/node_modules/array-extended/index.js:176:21)
    at Object.exports.compile (/home/steven/dev/xxxxxxxxxxx/node_modules/nools/lib/compile/index.js:171:5)
    at Object.nools.compile (/home/steven/dev/xxxxxxxxxxx/node_modules/nools/lib/index.js:288:21)
    at Object.<anonymous> (/home/steven/dev/xxxxxxxxxxx/src/node/nools.js:3:18)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)

I didn't have this problem with v0.1.9. To make sure I've downgraded and everything works A-OK.

Serialize and deserialize rules compiles

I assume that the rules compile is one of the most expensive operations in this process. I know I can create an in memory cache of my compiled rule flows which will keep this from happening too often. I'm wondering however if you've thought of a way to serialize and then deserialize a precompiled set of rules something like:

var flow = nools.compile(__dirname + "/count.nools");

fs.writeFile(__dirname + '/count.nc', JSON.stringify(flow), function (err) {
    if (err) throw err;
    console.log('It\'s saved!');
});

and then on the other side

var flow = nools.load(__dirname + '/count.nc');

Thanks again for this awesome tool, if you haven't thought about it but think it might be a good idea I'd be willing to take a shot at getting it to work. Of course if you see a reason why it couldn't work please let me know.

Multiple Rules don't seem to be firing

Here is my rules file

define Message {
    message : '',
    count : 0,
    constructor : function(message) {
        this.message = message;
    }
}

rule Goodbye {
    when {
        m : Message m.message == 'Goodbye';
    }
    then {
        console.log("Completed");
    }
}

rule Hello {
    when {
        m : Message m.message == 'Hello';
    }
    then {
        modify(m, function() {
            this.message = 'Goodbye';
            this.count++;
        });
    }
}

I would expect the message 'Completed' to be displayed once all the rules fire but it seems that only the 'Hello' rule fires.

Here is my code

var flow = nools.compile(__dirname + "/rule1.nools");
var Message = flow.getDefined('Message');
var session = flow.getSession();

session.assert(new Message('Hello'));

session.match().then(
    function() {
        console.log('Done');
    },
    function(err) {
        console.log('Error');
    }
);

session.dispose();

and here is what is displayed in my console

Done

I would expect that once the Hello rule fires it will modify the message and then execute the second rule.

Any clues?

cheers

Compiling nools from different sources

Nools should allow compiling flows and rules from different sources. Currently the rules compiler is reading the flow definition file from the file system, this should be changed to be read from a stream, this way people have the flexibility to store the rules wherever they please, in the file system, building them through an interface and storing them in memory, read them from a different location, etc...

The mechanism of having the rules stored only in file systems is neither flexible nor secure, rules might be a critical factor in evaluating business operations

How to query for list of facts in the session?

I have a rule that polls an external system at a fixed interval. This call returns a JSON array with objects that are converted to facts.

I want to assert some of these facts (these are new), I want to modify certain other facts (these are existing facts) and I want to retract other facts.

However, I can't seem to add the existing facts to the when { } clause, at least I think I can't. My facts use the Thing type. Is it possible to get an array of all Thing facts from the when { } clause?

How to pass classes to compile method?

According to the Readme, classes can be passed into the compile method for DSL files, but there is no documentation on the syntax the compile method expects.

Agenda Groups

A really useful feature would be have agenda groups support in the default rules language. This would allow groupings of rules that need to run together because of a logic reason that could be configured semantically. Currently the only way to achieve this is through a heavy use of priority/salience.

It would look like:

rule "A group a1 rule" {
  agenda-group: "a1",
  when { ...

rule "A group a2 rule" {
  agenda-group: "a2",
  when { ...

rule "Another group a1 rule" {
  agenda-group: "a1",
  when { ...

Then there would be a nools.groupArray which would be empty by default and you would add groups to this array in the order of desired execution such as nools.groupArray = ['a1', 'a2']. This ordering would result in a rules run of first attempting "A group a1 rule" and "Another group a1 rule" then attempting "A group a2 rule". Effectively running each agenda group as a separate rete tree that get's instantiated as the one prior is finished.

window error on server

I tried to use Nools with the helloWorld.js example from the examples, but I could not install it with npm install, so I decided to use it directly. I downloaded the nools.js and pleced it to the same folder with the helloWorld.js. Of course I modified the require sentence to this.

var nools = require("./nools.js");

Then I tries to run (node helloWorld.js ), but got reference error to 'window' in nools.js.

I guess the npm install problem is related to my system, but according to the manual nools can be used with the way I mentioned before.

$ node helloWorld.js

/node.js/nools.js:13
}).call(window);
^
ReferenceError: window is not defined
at Object.../ (
/node.js/nools.js:13:9)
at i (/node.js/nools.js:1:281)
at
/node.js/nools.js:1:444
at Object. (~/node.js/nools.js:1:462)
at Module._compile (module.js:456:26)
at Object.Module._extensions..js (module.js:474:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Module.require (module.js:364:17)
at require (module.js:380:17)

matchUntilHalt() uses a lot of CPU?

I've noticed that both in Node.js and in the browser matchUntilHalt() uses a lot of CPU. Is this observation correct and if so how can it be alleviated?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.