Self documenting JavaScript is good for you. We’ll get to how in a second, but first let’s define a few terms. By self documenting code I mean code that is so clear you don’t need to document it. I’m not talking about using JSDoc, or inline comments, or anything else that requires extra overhead to maintain.

To be clear, we’re not full-stop against traditional documentation, but I do submit that this pattern can be self documenting, and removes the need for as much documentation around your codebase.

If you’re here to learn about Regent, you can skip straight down to some practical regent examples. Ok, with that out of the way let’s get to it.

Let’s imagine we are writing a new JavaScript application. This application is going to care about the weather and give our users some helpful advice about how to prepare for it. Our client says that we want to suggest that the user wear a coat if today’s chance of precipitation is greater than 75, the temperature is currently below 60, or the low temperature is projected to go below 60. Take a quick look at this non-regent implementation of that logic.

// Do I need a coat?
if (
  data.chance_of_precipitation >= 75 || // Is it likely will have some precipitation today?
  (
    data.temp < 60 || // Is it already coat weather?
    data.temp_min < 60 // Will it become coat weather soon?
  )
) {
  console.log('You should probably wear a coat');
}

This code defines the logic we use to determine if it is “coat weather”. It contains two logical expressions, the first of which is checking if data.chance_of_precipitation is greater than or equal to 75. The second is an or grouping that will return true if data.temp or data.temp_min is less than 60.

If rain is expected, or it is cold, or it is predicted to get cold, you want to bring a coat.

The issue with writing code like this is that this logic needs to be documented. I’m sure everyone has worked on projects and found little pieces of code like this sprinkled around.

Inevitably, a stakeholder will ask the dreaded question: “Why is this application recommending that I bring a coat?”. These questions can be difficult to answer. In my experience I have to dive back into the code and find where this logic is written. This is problematic for two reasons.

  1. Usually, it takes a developer to read through the code and find the answer. This takes up the developer’s time.
  2. The question being asked isn’t a technical question, it’s a business logic question. The business experts should have access to the business logic, and they shouldn’t have to wait for a developer to find it for them.

Abstraction to the rescue

Lucky for us we can abstract this logic out. Check out this version.

NOTE: this example is using ES6 arrow functions, the const keyword, and object destructuring

const highChanceOfPrecipitation = chance_of_precipitation => chance_of_precipitation >= 75;
const coldEnoughForCoat = temp => temp > 60;
const mayBecomeColdEnoughForCoat = temp_min => temp_min > 60;
const doINeedACoat = ({ chance_of_precipitation, temp, temp_min }) => (
  highChanceOfPrecip(chance_of_precipitation) ||
  (
    coldEnoughForCoat(temp) ||
    mayBecomeColdEnoughForCoat(temp_min)
  )
);
// And we invoke it like this
if (doINeedACoat(input)) {
  console.log('You should probably wear a coat');
}

This is better. Breaking out this logic into single purpose functions and composing them together to satisfy your requirements is a great solution. Now we have a handle to the function doINeedACoat, and we can call it as needed in our application. If someone asks the question “Why is this application recommending that I bring a coat?”, we can head to the definition of doINeedACoat and find the answer. Also, writing code this way makes it super easy to test.

This pattern is not without its drawbacks however. Because it is abstracted, it requires some domain knowledge to find and implement. The most common way to handle this is by writing documentation. Also, it doesn’t solve the problem of a business person being able to find the answer to their question.

Our team asked how can we do better? How can we separate the “why” of our application (“why is this application recommending that I bring a coat?”), from the “how” (“how can we print the correct message to the screen?”)? Our answer became Regent.

Introducing Regent

Regent is a rules engine, built to help you separate the “how” from the “why” in your application. Developers typically focus on the how. “How will we program the application to meet the requirements?” The business is concerned with “why”. “Why is less than 60 degrees considered coat weather?” Business experts, in our case presumably meteorologists, are the ones qualified to make that decision.

Regent is an open source project currently being developed by Northwestern Mutual. Recently Northwestern Mutual has been releasing and contributing to open source projects. You can read more about the transformation in this article: Our Journey to Open Source.

Practically, Regent allows you to ask yes or no questions of a data structure. At the lowest level, Regent logic is written in tiny, single responsibility rules.

The Anatomy of a Regent Rule

const coldEnoughForCoat = {
  left: '@temperature.temp',
  fn: 'lessThan',
  right: 60
};

Notice that this rule exists only to let us know if it is cold enough for a coat. It does not care about precipitation, or the low temperature. The beauty of this is you may have multiple places in your application that care if it is cold enough for a coat. Because we have defined this rule, it becomes trivial to reuse this logic configuration.

Regent rules are composed of three parts.

left

left tells regent where to find this particular piece of data in the input data. The @ sign denotes that this value should be looked up at the path given.

fn

fn refers to the predicate you want to use. We’re not going to dive too deeply into this right now, just know that regent ships with a bunch of predicates, including equals, greaterThan, lessThan, includes (based on lodash.includes) and a few others. Regent also supports supports custom predicates.

right

right represents your comparison value. In the coldEnoughForCoat rule we provided a number, 60. You can also provide a lookup key to another spot in your data by using an @sign.

Now let’s take a look at how we would replicate our doINeedACoat logic from earlier.

Composing Regent Rules

// Is it currently cold enough to need a coat?
const coldEnoughForCoat = {
  left: '@temperature.temp',
  fn: 'lessThan',
  right: 60
};
// Is it supposed to become cold enough to need a coat?
const mayBecomeColdEnoughForCoat = {
  left: '@temperature.temp_min',
  fn: 'lessThan',
  right: 60
};
// compose those two into one
const coatTemperature = regent.or(coldEnoughForCoat, mayBecomeColdEnoughForCoat);
// Is rain likely?
const probablyWillRain = {
  left: '@chance_of_precipitation',
  fn: 'greaterThan',
  right: 75
};
// Finally we get our doINeedACoat rule
const doINeedACoat = regent.or(coatTemperature, probablyWillRain);

Each time we compose a rule we are assigning all the underlying logic to a new, human readable name. By the time we get to coatTemperature we no longer care that the mayBecomeColdEnoughForCoat rule is comparing the temp_min value against the constant 60. These details make up the “why” of our application and that cognitive load can now be more easily distributed among both technical and non-technical team members.

It is worth noting that should you need to look up the details of a low level rule it is easy to search down the logic chain because each rule is defined as a variable. Most text editors and IDEs will allow you to jump directly to each rule definition with a click.

Evaluating a Regent Rule

We can evaluate a regent rule to a boolean by using regent.evaluate.

const data = {
  temperature : {
    temp: 82.32,
    temp_min: 55.6,
  },
  chance_of_precipitation: 20,
}
if (regent.evaluate(doINeedACoat, data)) {
  console.log('You should probably wear a coat');
}

That’s it, we just evaluated a Regent rule.

Now, you may be thinking something like “that isn’t much different than the last example, and it seems like I had to write more code to do it.” You aren’t entirely wrong. But the good news is that the extra code is in the rule definitions, and serves as documentation to help us solve our “why” vs. “how” issue.

If you’re still having trouble seeing how those rule definitions can serve as documentation let’s look at another Regent helper: regent.explain.

regent.explain(doINeedACoat, data);

This will return

​​​​​(
  (
    (
      @temperature.temp->82.32 lessThan 60
    ) or (
      @temperature.temp_min->55.6 lessThan 60
    )
  ) or (
    @chance_of_precipitation->20 greaterThan 75
  )
)​​​​​

regent.explain parses your rules into human readable chunks. It takes a regent rule as the first parameter, and the data you called the regent rule with as an optional second parameter. In the output from our doINeedACoat rule you can see our nested logic. regent.explain gives a developer a nice way to revisit all the particular rules that make up a composed regent rule.

The Logic Table Pattern

This is where Regent really starts to pull its own weight. Regent logic tables are simply an array of object literals. Each object literal can be any shape, provided it has a rule property with a value equal to a regent rule.

Our logic table might look something like this

const coatLogic = [
  {
    value: 'You should probably wear a coat',
    rule: doINeedACoat
  },
  {
    value: 'No coat needed today',
    rule: regent.not(doINeedACoat),
  }
];

Regent provides two helper functions to help you consume logic tables: regent.find and regent.filter. Their functionality is based on Array.find and Array.filter. Using this pattern we can now implement our code like this.

console.log(regent.find(coatLogic, data).value);

So simple, so terse, so beautiful. We have now removed the if statement from our code entirely. We have moved that entire logic check into Regent. This reduces our unit test load greatly. You can choose to write tests to confirm that you have configured your rules, or logic tables correctly, but you don’t have a locally defined function to unit test anymore.

NOTE: regent.find returns the entire object from the logic table, so we are pulling the value property off of the result.

Let’s Get Even More Functional

Regent was written with functional programming in mind. Another pattern we love is exporting a curried query function from each logic table. This function knows about the shape of the objects in the logic table and makes the implementation less cumbersome. Check out this example.

// In this example we're using lodash.curry
import curry from 'lodash.curry';
// Our logic table from earlier
const coatLogic = [
  {
    value: 'You should probably wear a coat',
    rule: doINeedACoat
  },
  {
    value: 'No coat needed today',
    rule: regent.not(doINeedACoat),
  }
];
// Define our query. Notice how we are returning
// just the value property of the result.
const getCoatText = curry(regent.find)(coatLogic).value;
// Use it
const data = {
  temperature : {
    temp: 82.32,
    temp_min: 70.6,
  },
  chance_of_precipitation: 75,
}
console.log(getCoatText(data));

Regent in a React / Redux Application

I will leave you with one final example of a way my team is using regent to simplify our React/Redux application. We will use our coatLogic table from earlier.

import React from 'react';
import { connect } from 'react-redux';
import coatLogic from '../logic/coat-logic';
const CoatAdvice = ({ coatText }) = (
  <h1>{coatText}</h1>
);
const mapStateToProps = state => ({
  coatText: regent.find(coatLogic, state),
});
export default connect(mapStateToProps)(CoatAdvice);

We now have access to the coat text in the props of >CoatAdvice>. Again, we are removing bug prone switch statements and if checks from our code by allowing Regent to control those decisions.

In Conclusion

Regent was not written to be opinionated, it exists to help you move all of your business logic into a common structure. It isn’t trying to rule with an iron fist, and it isn’t going to make you completely restructure your application all at once.

I’m excited to write a more in-depth post about using Regent queries in the react-redux mapStateToProps function, so look for that in the future. If you want to know more about Regent you can check out Regent on GitHub, or NPM.

As a reminder Regent is open source. If you have any ideas, criticisms, or feature requests you can swing on over to https://github.com/northwesternmutual/regent and become a contributor.