Skip to main content
Deno 2 is finally here 🎉️
Learn more

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 notifying changes in the states of the FactBaseElements.

This implementation provides state-of-the-art features of NOP, with only 500 lines of code, implemented in TypeScript, exploring the current limits of object orientation and imperative programming; note that it is an implementation of the paradigm, not an optimization of its computational model. 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.

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 extend the FactBaseElement class and use the superclass method to access and set the observable values. The FactBaseElement class saves all these values ​​in an internal observable map and the Premises of a Condition have as one of its parameters an instance of subtype (or type, FactBaseElement is also instantiable) 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".

This implementation is built by exploring the concept of concurrent programming rather than parallel programming. This approach can result in some problems. For example: If a change in the state of an FactBaseElement triggers the evaluation of a Rule, and at the time that Rule is being evaluated there is again a change in the state of the FactBaseElement, the evaluation of the Rule, its Action or the evaluation of its Triggers can be inconsistent. To control this, when a Rule is evaluated, the context (the attributes and values ​​of all FactBaseElemens) is cloned using the structured clone algorithm, the same algorithm that JavaScript uses to exchange messages between threads. You can disable this mechanism:

Rule.preserveState = false;

This will speed up execution, but can make it more difficult to maintain application consistency.

See also options for instantiating a Rule:

type RuleOptions = {
  condition: Condition;
  action: (messages?: Map<Rule, any>) => Promise<any> | any;
  delay?: number;
  depends?: Rule[];
};

Conditions are implemented in a tree structure, easy for humans to understand. For each Condition; 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.
}
*/

There is also an extension interface for named functions. Which are used as cuttomized functions in Premises and Conditions. See how to use:

Rule.registerExtensions([deepEqual, customFunc2]); //Register at the beginning of the program all your custom functions.

/*
In Premises:
deepEqual = function with name "deepEqual", ex: 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: 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 different types of Conditions together, ex: 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.

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 #FBEattrMap: {
    [key: string]: Map<string, Set<Rule>>;
  } = {};
  static #extensions: {
    [key: string]: Function;
  } = { "deepEqual": deepEqual };
  static preserveState = true;
  #transpiledCondition: (context: FactsContext) => Promise<boolean>;

Sample application

This program contains an example of an application called “Target shooting”.

import {
  Condition,
  deepEqual,
  FactBaseElement,
  FBEvalue,
  Premise,
  Rule,
} from "https://deno.land/x/script_nop/mod.ts";
class Shooter extends FactBaseElement {
  constructor(fbeName: string) {
    super(fbeName);
  }
  shoot() {
    super.set(
      {
        gun: {
          bullets: 5,
          pull_trigger: true,
        },
        target: true,
      },
    );
  }
}

const shooter1 = new Shooter("shooter1_name");

const rule1 = new Rule(
  {
    condition: {
      premise: {
        fbe: "shooter1_name",
        attr: "gun.bullets",
        is: ">",
        value: 0,
      },
    },
    action: () => console.log("loaded gun!!!"),
  },
);

shooter1.shoot();

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)
iwr https://deno.land/install.ps1 -useb | iex
# run project example:
deno run example.ts
# bundle scriptNOP lib to any runtime or web browsers:
deno bundle mod.ts nop.js

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: 10.1109/LA-CCI.2015.7435978. 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!