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 311 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 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. This dependency is also implemented in the trigger style. 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",
},
},
},
);
fbe.get("a"); //returns object "a".
fbe.delete("a"); //delete object "a".
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 does not handle circular references yet!. See examples of Conditions:
//------------------------- TYPES OF CONDITIONS ----------------------
//----------------WITH ONE PREMISE:
const c: Condition = {
premise: {
fbe: shooter1,//FactBaseElement instance.
attr: "target.person.age", //path notation
is: ==, //"==", ">", "<", etc. Or: function name (registered extension).
value: true, //reactive FBEvalue or non-reactive constant.
},
}
//----------------WITH ONE SELF-EVALUATED PREMISE:
//The value of the respective attr is already the result of the premise.
const c: Condition = {
premise: {
fbe: layer1, //FactBaseElement instance.
attr: "target.person.age" //path notation.
}
}
const c: Condition = {
premise: {
fbe: shooter2, //FactBaseElement instance.
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: c,//c 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,
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,
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,
attr: "neurons.0", //paths with .N is valid for vectors
is: "sumOfWeights", //custom function name in Premise, input is fbe.attr
},
},
],
},
},
{
premise: {
fbe: shooter1,
attr: "gun.distance",
},
min_threshold: 0.2, //fuzzy parameter, combinable with any type of Condition
},
{
premise: {
fbe: shooter1,
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: FactBaseElement;
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<FactBaseElement, Set<Rule>>;
} = {};
static #extensions: {
[key: string]: Function;
} = {};
#transpiledFBEs: FactBaseElement[] = [];
#transpiledCondition: () => boolean;
Sample application
This program contains an example of an application called "Target shooting".
class Shooter extends FactBaseElement {
shoot() {
super.set(
{
gun: {
bullets: 5,
pull_trigger: true,
},
target: true,
},
);
}
}
const shooter1 = new Shooter();
const rule1 = new Rule(
{
premise: {
fbe: shooter1,
attr: "gun.bullets",
is: ">",
value: 0,
},
},
() => 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
[2] 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