miniNOP

Reference code for miniNOP, a Minimum required interface for implementing the notification-oriented programming paradigm (NOP[1]).

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. The simplification of programming paradigms by defining a minimal interface to allow implementations in several languages ​​is a promising attr. An example is miniKanren, which defines a minimal interface for logic programming, which allowed more than 170 implementations in the most diverse languages. In miniKanren, the reference code for implementation in the authors book takes up only 2 pages.

With that in mind, this implementation was developed, with only 309 lines of code, implemented in TypeScript, exploring the current limits of object orientation and imperative programming. The implementation has no dependencies on other libraries and can be used in any TypeScript/JavaScript runtime or modern browsers. Also, this implementation IS REACTIVE IN DEPTH and optionally accepts FUZZY FUNCTIONS and CUSTOM FUNCTIONS like weighted weights of a NEURON, 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.
*/
fbe.set(
  {
    a: {
      b: {
        c: "bar",
      },
    },
  },
);

Conditions are implemented in a tree structure, easy for humans to understand. For each Condition, you must choose only one of the keys: "not", "and", "or", "xor" or "premise". Note that the "." is reserved in this implementation for path notation. This implementation does not handle circular references yet!. See examples of Conditions:

/*
------------------------- PREMISES ----------------------
premise: {
  fbe: FactBaseElement instance.
  attr: path notation, ex: "target.person.age".
  is: "==", ">", "<", etc. Or: function name (registered extension).
  value: reactive FBEvalue or non-reactive constant.
},

SELF-EVALUATED PREMISE:
//the value of the respective attr is already the result of the premise.
premise: {
  fbe: FactBaseElement instance.
  attr: path notation, ex: "target.person.age".
}

------------------------- CONDITIONS ----------------------
//with one premsie
{
  premise: object of type Premise.
}
//with optional values ​​for fuzzy logic:
{
  min_threshold: floating point (or any number) value for fuzzy logic (optional), "if condition < min_threshold".
  max_threshold: 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.
  exactly: The result of the expression must be equal to the value.
}
//with OR, AND, XOR
{
  or | and | xor: [
    array of sub conditions.
  ]
}
//with custom function
{
  is: "function name (registered extension) or operator (+, *, etc)",
  sub_conditions: [
    array of sub conditions.
  ]
}
//with negation
{
  not: object of type Condition.
}
*/
//EXAMPLE:
const c: Condition = {
  and: [
    {
      premise: <Premise> { //Types Premise and FBEvalue can be inferred, but we make it explicit to explain
        fbe: shooter1,
        attr: "gun.bullets ",
        is: ">",
        value: <FBEvalue> { fbe: shooter2, attr: "gun.bullets" }, //reactive FBEvalue
      },
    },
    {
      or: [
        {
          not: {
            premise: {
              fbe: shooter1,
              attr: "target",
              is: "==",
              value: true, //Non-reactive constant
            },
          },
        },
        {
          premise: {
            fbe: shooter1,
            attr: "gun.pull_trigger",
            is: "==",
            value: true,
          },
        },
      ],
    },
  ],
};

There is also an extension interface for named functions. The function can have two parameters, or only one (when "value" is always omitted for example). See how to use:

Rule.registerExtensions([deepEqual]); //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" and "b" are already evaluated premises. "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:
//deepEqual = function with name "deepEqual", ex: function deepEqual(a: any[]): any { ...
//"a" is a is an array of already evaluated conditions ("sub_conditions" parameter).
const c: Condition = {
  is: "customFunc", //In conditions, 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: "character",
        is: "deepEqual", //function name here
        value: { name: "joe", age: 25 }, //Non-reactive constant, but it could also be an FBEvalue.
      },
    },
  ],
};

Combination of simple logic, fuzzy logic and custom functions:

const c: Condition = {
  or: [
    {
      not: {
        premise: {
          fbe: layer1,
          attr: "neurons.0", //paths with .N is valid for vectors
          is: "sumOfWeights", //custom function name
          min_threshold: 0.2, //fuzzy
        },
      },
    },
    {
      premise: {
        fbe: shooter1,
        attr: "gun.pull_trigger",
        is: "==",
        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/miniNOP.git
# enter the project directory
cd miniNOP
# 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 miniNOP 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