Skip to main content

scriptNOP

A framework for notification-oriented programming paradigm (NOP[1]) implemented in TypeScript.

In the NOP paradigm, in a simplified way, there are several FactBaseElements and several Rules. Rules have premises that check if FactBaseElements Attributes satisfy a Condition. If the Condition is satisfied, the Action is executed, calling a function and being able to change the Attributes of the FactBaseElements again (and finally being able to activate other Rules); that is, the Rules has the function of check changes in the states of the FactBaseElements and perform Actions.

This implementation provides state-of-the-art features of NOP, with only 854 lines of code, implemented in TypeScript, exploring the current limits of object orientation and imperative programming, parallel programming and concurrent programming. The implementation has no dependencies on other libraries and can be used in any TypeScript/JavaScript runtime or browsers. Also, this implementation is REACTIVE IN DEPTH and optionally accepts FUZZY[2] parameters and CUSTOM FUNCTIONS like sum of a weighted input of a NEURON[3], and you can still combine it all at the same time.

Contents

Sample application

This program contains an example of an application called “Target shooting”. There is the main thread (state manager), where all the FactBaseElement are, and there are the secondary threads where the Rules are.

main.ts (main thread):

import {
  App,
  delay,
  FactBaseElement,
} from "https://deno.land/x/script_nop/mod.ts";
App.init({
  numThreads: 1,
  extensionsURLs: [ //for URL to local file: new URL("./my_file.js", import.meta.url).href
    "https://deno.land/x/script_nop/src/extensions/deepEqual.ts",
  ],
  rulesURL: "https://deno.land/x/script_nop/example/rules.ts",
  onRuleMsg: (rule: any, msg: any) =>
    console.log(
      `Received message "${JSON.stringify(msg)}" from Rule "${rule}"`,
    ),
});
/*
  * Example: Target shooting aplication
  */
class Shooter extends FactBaseElement {
  constructor(fbeName: string) {
    super(fbeName);
  }
  shoot() {
    super.set(
      {
        gun: {
          bullets: 5,
          pull_trigger: false,
        },
        target: false,
      },
    );
  }
}
const shooter1 = new Shooter("shooter1_name");
await delay(3000); //Wait for all threads to be started
shooter1.shoot();

rules.ts (secondary threads):

import { Rule } from "https://deno.land/x/script_nop/mod.ts";
const rule1 = new Rule(
  {
    name: "rule1_name",
    condition: {
      premise: {
        fbe: "shooter1_name",
        attr: "gun.bullets",
        is: ">",
        value: 0,
      },
    },
    action: (context: any, messages: any) => {
      console.log("loaded gun!!!");
      const bullets = context["shooter1_name"].get("gun.bullets"); // get fbe value.
      //const msgRule2 = messages["rule2_name"]); //get message from "rule2_name" (if "rule2_name" is a dependency).
      context["shooter1_name"].set({ // set fbe value, changes will be automatically sent to the state manager thread.
        target: true,
      }, "IGNORE_MISSING"); //with "IGNORE_MISSING", missing attributes in will not be considered excluded.
      //Messages are control mechanisms for the NxN relationship between Rules.
      return { gun_loaded: true }; //send message to the state manager thread, if you have Rules that depend on this rule activated, they will also receive this message.
    },
  },
);

To avoid problems with threads, start the “rules.ts” file with the Rules instantiations, putting operations like “await” at the end of the file, after such instantiations.

Defining Conditions

Conditions are implemented in a tree structure, easy for humans to understand. Note that the “.” is reserved in this implementation for path notation, and this implementation handles circular references. See examples of Conditions:

//------------------------- TYPES OF CONDITIONS ----------------------
//----------------WITH ONE PREMISE:
const c: Condition = {
  premise: {
    fbe: "shooter1_name", //FactBaseElement name.
    attr: "target.person.age", //path notation
    is: ==, //"==", ">", "<", etc. Or: function name (registered extension).
    value: true, //non-reactive constant.
  },
}
//----------------WITH ONE PREMISE (WITH REACTIVE VALUE):
const c: Condition = {
  premise: {
    fbe: "shooter1_name", //FactBaseElement name.
    attr: "target.person.age", //path notation
    is: ==, //"==", ">", "<", etc. Or: function name (registered extension).
    value: { //reactive FBEvalue
      fbe: "shooter2_name",
      attr: "target.person.age",
    },
  }
}
//----------------WITH ONE SELF-EVALUATED PREMISE:
//The value of the respective attr is already the result of the premise.
const c: Condition = {
  premise: {
    fbe: "shooter2_name", //FactBaseElement name.
    attr: "target.person.age" //path notation.
  }
}
const c: Condition = {
  premise: {
    fbe: "layer1_name", //FactBaseElement name.
    attr: "neurons.0", //path notation.
    is: "sumOfWeights", //custom function name, function out is result of the premise, fbe.attr is input of function
  }
}
//----------------WITH OR, AND, XOR
const c: Condition = {
  and: [ //keys: "or", "and", "xor"
    //ARRAY of sub conditions.
  ]
}
//----------------WITH custom function
const c: Condition = {
  is: "+", // "function name (registered extension) or operator (+, *, etc)",
  sub_conditions: [ //this vector is the input parameter of the function
    //ARRAY of sub conditions.
  ]
}
//----------------WITH negation
const c: Condition = {
  not: c2, //c2 is one object of type Condition.
}
//----------------WITH OPTIONAL parameters ​​for FUZZY logic:
//Fuzzy parameters are optional and combinable with any type of Condition.
const c: Condition = {
  // ... (Condition parameters) ...
  min_threshold: 0.2, //floating point (or any number) value for fuzzy logic (optional), "if condition < min_threshold".
  max_threshold: 0.8, //floating point (or any number) value for fuzzy logic (optional), "if condition > max_threshold", you can set a defined range, defining min_threshold and max_threshold at the same time.
}
const c: Condition = {
  // ... (Condition parameters) ...
  exactly: 0.5, //The result of the expression must be equal to the value.
}
*/

Condition with extensions

There is also an extension interface for named functions, which are used as cuttomized functions in Premises and Conditions. These extensions are defined at the beginning of the main thread by the “extensionsURLs” parameter, but they can also be defined manually in the Rules file:

Rule.registerExtensions([customFunc2]);

How to use extensions:

/*
In Premises:
deepEqual = function with name "deepEqual", ex: export default function deepEqual(a: any, b: any): any { ...
"a" is the result of "fbe.attr" and "b" is the result of "fbe.value"; "b" is optional for self-evaluated Premises.
*/
const c: Condition = {
  premise: {
    fbe: "shooter1_name",
    attr: "character",
    is: "deepEqual", //FUNCTION NAME HERE
    value: { name: "joe", age: 25 }, //Non-reactive CONSTANT, but it could also be an FBEvalue
  },
};
/*
In Conditions:
custonFunc = function with name "custonFunc2", ex: export default function custonFunc2(a: any[]): any { ...
"a" is an array of result of Conditions ("sub_conditions" parameter).
*/
const c: Condition = {
  is: "customFunc2", //FUNCTION NAME HERE, the "is" can also be operators like "+", "*", etc.
  sub_conditions: [ //"sub_conditions" only exists when the "is" attribute in a Condition is filled
    {
      premise: {
        fbe: "shooter1_name",
        attr: "gun.bullets",
        is: "==",
        value: true, //Non-reactive constant, but it could also be an FBEvalue.
      },
    },
  ],
};

Combination of Conditions

A combination of different types of Conditions together is possible. Example with simple logic, fuzzy logic and custom functions:

const c: Condition = {
  or: [
    {
      not: {
        is: "ReLU", //custom function name in Condition, input is sub_conditions Array
        sub_conditions: [
          {
            premise: {
              fbe: "layer1_name",
              attr: "neurons.0", //paths with .N is valid for vectors
              is: "sumOfWeights", //custom function name in Premise, input is fbe.attr
            },
          },
        ],
      },
    },
    {
      premise: {
        fbe: "shooter1_name",
        attr: "gun.distance",
      },
      min_threshold: 0.2, //fuzzy parameter, combinable with any type of Condition
    },
    {
      premise: {
        fbe: "shooter1_name",
        attr: "gun.pull_trigger",
        is: "==", //simple logic
        value: true,
      },
    },
  ],
};

In the library package the extension functions “deepEqual”, which checks in depth if two objects are the same, i.e. compares their parameters, subparameters and etc. It is possible for example an extension function that represents a sum of weighted weights of a neuron, it can also be combined with fuzzy logic for the activation threshold of the same.

Rules

See also options for instantiating a Rule:

type RuleOptions = {
  name: string;
  condition: Condition;
  action: (
    context: { [key: string]: FactBaseElement },
    messages: Messages,
  ) => Promise<any> | any;
  delay?: number;
  depends?: string[];
};

Instructions to run this project

Basically you just need to clone the project and install the Deno runtime.

# clone project
git clone https://github.com/hviana/scriptNOP.git
# enter the project directory
cd scriptNOP
# install Deno (Mac, Linux)
curl -fsSL https://deno.land/install.sh | sh
# install Deno (Windows/PowerShell)
iwr https://deno.land/install.ps1 -useb | iex
# run project example:
deno run --unstable --allow-read --allow-net --allow-write main.ts
# bundle scriptNOP lib to any runtime or web browsers:
deno bundle mod.ts nop.js

Particularities of this implementation

In NOP we originally have the following entities: FactBaseElements, Attributes, Premises, Conditions, Rules, Actions, Instigations and Methods. In this implementation, we do not have entities to represent Attributes, Instigations and Methods, as they have been removed for convenience. So, we only have FactBaseElements, Premises, Conditions, Rules, and Actions. Actions directly represent a function reference. All system observable agents are instances of FactBaseElement.

When implementing NOP in the von Neumann architecture with imperative programming we have the terrible complexity of O(n^3) in the worst case -> O(FactBaseSize(O(n)) * nPremises(O(n)) * nRules(O(n))) (this limit is theoretical and rarely happens in practice). This implementation also don’t use “infinite loops” and has a global FBE+Attribute x Premises MAP that associates an instance of FactBaseElement and a respective Attribute name to the Premises that use it. When an Attribute is modified, at the end of the modification it notifies the respectives Rules according to the FBE+Attribute x Premises MAP, in trigger style, no need to loop through the fact base (FactBaseElemens Attributes) to check for changes. With these optimizations we reduced the worst case upper bound to O(n^2) -> O(FactBaseSize(O(1)) * nPremises(O(n)) * nRules(O(n))). When an Attribute is modified, only the Rules that have Conditions with Premises that use the respective FactBaseElement and modified Attribute are re-evaluated. This doesn’t change the average case upper bound, but it helps a lot in running the program. In the average case, the complexity is O(n) -> (numPremises + numRules). This implementation also considers the dependency of Rules: A Rule B can depend on a Rule A, and the Rule B is only evaluated automatic if the Rule A is satisfied, and this dependency is also implemented in the trigger style. A Rule can have several dependencies and be dependencies on several others. To implement this NxN relationship on the Rules, the concept of messages was implemented. When a Rule is executed, the return of its action is passed as a message to the other Rules that depend on it. A Rule knows that it has satisfied its dependencies when it has received messages from all dependencies. It is always considered the last message sent, and when the rule is activated, your received messages is cleared. The computational architecture of the implementation can result in a “freeze” of the program if infinite changes of FactBaseElements states start, given the respectives Actions. To minimize this problem and at the same time implement the priority idea of Actions and Rules, when creating a Rule it is possible to insert an optional delay for its Action. Note that there is no need for a “Dispatcher” to queue notifications, as such notifications are implemented using ASYNC functions with delay. Depth reactivity allows modifications to an FactBaseElemens object’s sub-attributes to represent changes in the FactBaseElemens object’s state, triggering the respective Rules associated with it. For example:

//initial values.
fbe.set(
  {
    a: {
      b: {
        c: "foo",
      },
      d: true,
    },
  },
);
/*
Rules that use "a", "a.b" or "a.b.c" will be re-evaluated.
Rules that use only "a.d" are not re-evaluated, since the value of "a.d" has not been modified.
*/
fbe.set(
  {
    a: {
      b: {
        c: "bar",
      },
      d: true,
    },
  },
);
fbe.get("a.b"); //returns object "a.b".

The code is very dense, although every detail has been thought of in order to favor readability and avoid replication. With TypeScript, we have a new way of defining types and programming in an object-oriented style compared to classic object-oriented languages ​​such as Java and C++, which drastically reduces the amount of code. See the following code snippet:

export interface FBEvalue {
  fbe: string;
  attr: string;
}
export interface Premise extends FBEvalue {
  is: string;
  value: any | FBEvalue;
}
// ...
interface ConditionWithXor extends FuzzyCondition {
  xor: [Condition, Condition, ...Condition[]]; //min 2 conditions
}
export type Condition =
  | ConditionWithNot
  | ConditionWithPremise
  | ConditionWithAnd
  | ConditionWithOr
  | ConditionWithXor
  | ConditionWithFunc;
//...
export class Rule {
  static #extensions: {
    [key: string]: Function;
  } = {};
  static initialized: boolean = false;
  #transpiledCondition: (
    context: { [key: string]: FactBaseElement },
  ) => Promise<boolean>;

References

[1] J. M. Simão, C. A. Tacla, P. C. Stadzisz and R. F. Banaszewski, “Notification Oriented Paradigm (NOP) and Imperative Paradigm: A Comparative Study,” Journal of Software Engineering and Applications, Vol. 5 No. 6, 2012, pp. 402-416. doi: https://www.doi.org/10.4236/jsea.2012.56047

[2] Melo, Luiz Carlos & Fabro, João & Simão, Jean. (2015). Adaptation of the Notification Oriented Paradigm (NOP) for the Development of Fuzzy Systems. Mathware& Soft Computing. 22. 1134-5632. url: https://www.researchgate.net/publication/279178301_Adaptation_of_the_Notification_Oriented_Paradigm_NOP_for_the_Development_of_Fuzzy_Systems

[3] F. Schütz, J. A. Fabro, C. R. E. Lima, A. F. Ronszcka, P. C. Stadzisz and J. M. Simão, “Training of an Artificial Neural Network with Backpropagation algorithm using notification oriented paradigm,” 2015 Latin America Congress on Computational Intelligence (LA-CCI), 2015, pp. 1-6, doi: https://doi.org/10.1109/LA-CCI.2015.7435978

About

Author: Henrique Emanoel Viana, a Brazilian computer scientist, enthusiast of web technologies, cel: +55 (41) 99999-4664. URL: https://sites.google.com/site/henriqueemanoelviana

Improvements and suggestions are welcome!