JavaScript: Separate Predicate Functions
Tags: webdev, javascript
Published: 2024-05-15 22:45:01

Functions can be long and complicated, and the reasons we execute them can change. Separating the _reason_ from the _action_ can make code more reusable and has other benefits.

## What's a Predicate?

Predicates are functions that return a Boolean `true` or `false`. If it helps, you can think of these as the logic in the parentheses of an `if (...)` statement. They are the _condition_, captured as a function.

## Mixing Responsibilities

This function is responsible for deciding if we do work and what the work is:

```javascript
const doImportantTask = (data) => {
  if (data
    && data.hasChanged
    && !data.isTooOld
    && !data.isUpdateInProgress) {
    const { crucialValue } = data;
    // ...many more steps
    return processedValue;
  }
};
```

We can easily make the action separate, and reorganize the conditions into early returns – mostly for a style change.

```javascript
const performImportantCalculation = (value) => {
  // ...many more steps;
  return processedValue;
};

// Shoot later. Ask questions first.
const doImportantTask = (data) => {
  if (!data) return;
  if (!data.hasChanged) return;
  if (data.isTooOld) return;
  if (data.isUpdateInProgress) return;

  return performImportantCalculation(data.crucialValue);
};
```

Even though we separated `performImportantCalculation`, we still have two jobs inside `doImportantTask`: deciding and running. What if those same conditions apply to some other function we need to run? Let's isolate the predicate so it can be reused.

```javascript
// Only need one optional chaining with &&
const isDataReady = (data) => data?.hasChanged
  && !data.isTooOld
  && !data.isUpdateInProgress;

const performImportantCalculation = (value) => {
  // ...many more steps;
  return processedValue;
};

const doImportantTask = (data) => {
  if (isDataReady(data)) {
    return performImportantCalculation(data.crucialValue);
  }
};
```

This is a contrived example, but we now have a _predicate_ `isDataReady` and an _action_ `performImportantCalculation` that are not coupled together. This means we can update – and _test_ – the condition separate from the action, and we can easily combine one action with different conditions or one condition with different actions.

## Predicates and Hot Loops

Predicates can be even more helpful in code that runs frequently, like middleware. Rather than loading the large function which contains both conditions and actions, the smaller predicate can run to decide if you need to call the larger action.

You can see this pattern inside Redux Toolkit. [`startListening`](https://redux-toolkit.js.org/api/createListenerMiddleware#startlistening) takes a `type`, `actionCreator`, `matcher`, or `predicate`. These are all _conditions_ to decide if we should bother running the `effect` – the _action_. Doing this saves the program from loading the `effect` function. If they were combined, how many times might we load the effect just to perform exit early?

### Over-optimized?

As with optimization, you can go overboard. There may not be any performance benefit if your code runs infrequently or the condition is almost always true. It might matter in middleware, view-binding, game engines, or other high-traffic code...places that could process many actions or respond to user interaction.

Performance impact is only one consideration, though. 

## Maintainability

Small, reusable, and _composable_ functions allow us to avoid writing the same logic multiple times. By breaking it apart we can focus on the writing only the new conditions, new actions, and new ways the pieces fit together.

When you start grouping and naming the conditions separately, the condition names tend to focus on _why_ and the actions focus on _what_. This can make it easier to follow the logic of your program over time.

```javascript
// Predicates
const isDataReady = () => { /* ... */ };
const isDeviceOnline = () => { /* ... */ };
const isUserLoggedIn = () => { /* ... */ };

// Actions
const showLoginScreen = () => { /* ... */ };
const fetchUserData = () => { /* ... */ };
const startApp = () => { /* ... */ };

// Now, assemble the pieces.
```

### Functional Style

This also goes well with functional programming style. We've broken the parts down into easily consumed pieces. With a few helpers we can assemble them.

```javascript
const isGuest = and(isDeviceOnline, not(isUserLoggedIn));
const isUserOnline = and(isDeviceOnline, isUserLoggedIn);

doIf(isGuest, showLoginScreen)(state);
doIf(isUserOnline, fetchUserData)(state);
```

## Conclusion

Small, single-responsibility functions are nothing new, but sometimes a different perspective or even just a reminder on things you already know can be helpful.

Do you create predicates like these in your work? I'd love to hear about how your projects handle repetition and reusability.

## Notes
Cover image generated with Microsoft Designer Image Creator.