Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Condition Sharing #339

Merged
merged 4 commits into from
Jul 12, 2023
Merged

Conversation

chris-pardy
Copy link
Collaborator

@chris-pardy chris-pardy commented Jul 10, 2023

This introduces the concept of Condition sharing which allows regular conditions that are given a well-known name and to be added to the engine. Rules can reference the condition by including a condition property on a condition.

example based on #296

engine.setCondition('isMinor', {
  all: [
    {
        fact: 'age',
        operator: 'lessThan',
       value: { fact: 'drinkingAge' }
    }
  ]
});

engine.addRule({
   conditions: {
      all: [
          { not: { condition: 'isMinor' } },
         {
             fact: 'currentTime',
             operator: 'lessThanInclusive',
             value: { fact: 'lastCall' }
         }
     ]
  },
    event: {
      type: 'AlcoholicBeverageSelection'
   }
});

// condition references can be top-level conditions so it's possible for a rule to just be a condition reference.
engine.addRule({
 conditions: {
    condition: 'isMinor'
  },
  event: {
     type: 'SoftDrinkSelection'
  }
})

This addresses #336, #296, and #263

A few things related to this:

Why do we need this at-all

The ability to capture common conditions and compose rules is useful as a first-class function of this rules engine. As rules reach increased levels of complexity common patterns emerge that should be re-used rather than repeated. One option to do this is to add a "pre-processing" step outside the rules engine that would build up rules from component parts. However this means that someone inspecting the rules as they're run would need to understand the behavior of this pre-processing. Additionally having this be supported by the rules engine allows for more universal editing tools to adopt this as something that can be edited.

Condition references vs. rule-chaining

The primary difference here is that the composition can be communicated using JSON because conditions are fully expressible as JSON. This prevents the javascript code from needing to know about specific rules to setup specific event handlers. This allows us to cleanly separate javascript code that configures the rules engine from the rules themselves. Additionally there is no need to specify rule priority when using conditions as the condition references will be resolved at runtime.

Why condition references and not something with facts

I had originally suggested using a "conditional fact" or other construct to capture the idea of re-used logic. This was nice because it matched concepts that already existed in the rules engine. However it fails the equivalency test. Extracting re-used conditions into a fact would cause the rule results to be different. I liked that the use of Conditions and the approach of de-referencing the condition references at runtime would keep the same structure to the rule results even after refactoring.

Concern with duplicated work.

Condition references are dereferenced and evaluated within each rule they're part of. That means that if a condition is used in 10 rules it will be evaluated 10 times. I'm not concerned about this as the majority of the work will be captured by the Fact value caching in the almanac. The work that is left to be done ought to be very quick. While the possibility of result caching does exist as a future improvement adding it here seemed unnecessarily complex.

Christopher Pardy added 2 commits July 10, 2023 12:58
fix the link to the boolean expressions that broke after not was introduced.
This will allow you to npm install examples and actually run them.
Copy link

@RoboCafaz RoboCafaz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A very simple solution for adding additional composability options to the rules engine. I'm a fan.

The lack of result caching is a bit of a bummer but that's something that could be iterated upon in future updates.

types/index.d.ts Outdated Show resolved Hide resolved
docs/rules.md Outdated Show resolved Hide resolved
docs/rules.md Outdated Show resolved Hide resolved
docs/rules.md Outdated Show resolved Hide resolved
src/engine.js Outdated Show resolved Hide resolved
Copy link

@matthew-mahony matthew-mahony left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such a simple solution to something that will be super useful!

Copy link
Owner

@CacheControl CacheControl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Underlying implementation looks good.

Per comments let's think hard about the naming and api we're exposing.

README.md Outdated
@@ -29,6 +29,7 @@ A rules engine expressed in JSON

* Rules expressed in simple, easy to read JSON
* Full support for ```ALL``` and ```ANY``` boolean operators, including recursive nesting
* Rule composition / inheritance with the use of shared conditions
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this as an advanced feature that grows in importance as users begin using the library more extensively; seems like a highly specific thing to mention up front in the readme. Recommend deleting this line

docs/engine.md Outdated
Adds a shared condition to the engine. Rules may include references to this shared condition.

```javascript
engine.addSharedCondition('validLogin', {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not simply addCondition()? Adding new terminology into the mix suggests to the user they need to understand the difference between a shared condition vs a non-shared condition; wondering if we can maintain the single concept of a condition and simply offer this as an upgrade and additional way to use conditions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was doing some googling to try to see what this was called in other rules engines and there doesn't seem to be a good name for these in wide use. So I'm ok with condition as the name I actually like that in a future state this could be used to add new Condition subclass constructors which would let make it even more extensible.

docs/engine.md Outdated
condtions: {
all: [
{
uses: 'validLogin'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if condition: would be more ergonomic, as it clarifies to the user the type of the identifier they need to place in the value, and mentally ties it back to engine.addSharedCondition(). It'd also be consistent with other existing properties such as fact: <id>, operator: <id>. thoughts?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the more time I spend with it the less I like uses. I'm fine with either condition or ref as I think there a more natural term. The one thing with condition is that it means a rule could be { conditions: { condition: <id> } } how do we feel about that?

@@ -10,6 +10,6 @@
"author": "Cache Hamm <cache.hamm@gmail.com>",
"license": "ISC",
"dependencies": {
"json-rules-engine": "6.0.0-alpha-3"
"json-rules-engine": "../"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

src/engine.js Outdated Show resolved Hide resolved
@CacheControl
Copy link
Owner

CacheControl commented Jul 11, 2023

I think you'll see to add a line to the toJSON helper in order to ensure the uses: properly serializes

Add Support for Conditions which are added to the engine and then
referenced in various rules.
Condition references can be used as top-level conditions in a rule
and can be used recursively.
@chris-pardy chris-pardy changed the title Shared Conditions Condition Sharing Jul 12, 2023
@chris-pardy
Copy link
Collaborator Author

@CacheControl all comments addressed, looking for a final sign-off before I merge this and put together a point release.

Add documentation covering the use and advantages of condition sharing.
Specifically on the ability to re-use the same condition across multiple
rules.
@chris-pardy chris-pardy merged commit e500c1d into CacheControl:master Jul 12, 2023
3 checks passed
@chris-pardy chris-pardy deleted the shared-conditions branch July 12, 2023 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants