doIf (if/then/else): JavaScript Functional Programming
Tags: javascript, webdev, programming
Published: 2023-11-15 21:57:36

Functional programming can simplify code and make it easier to spot defects. Today we're exploring converting imperative `if` statements.

## Terms

When we talk about [`if...else`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/if...else) statements, we usually talk about the *if condition*, the *then block*, and the *else block*. Sometimes these we use then or else *statements*, but blocks are more common.

Functional Programming has origins in logic, so we see words like `predicate`, `consequent`, and `alternative`. These are functions for the "if", "then", and "else", respectively.

- **predicate** - The “if” function that asserts something is true or false.
- **consequent** - The “then” function to run if the predicate is true.
- **alternative** - The “else” function to run if the predicate is false.

A few other words we might see along the way:

- **nullary** - A function that takes no arguments, like `() => true`
- **unary** - A function that takes one argument, like `(x) => x + 1`

## Building The Base

### Requirements

Like any project, we start by figuring out what this function should do. The basics are pretty clear; it's if/then/else, but with functions. Let's write it out, anyway.

1. It must accept `predicate`, `consequent` and `alternative` functions.
2. It must return a *new function*. This will accept arguments and run the logic.
3. When the new function called, it must run the predicate with the arguments.
4. Based on the predicate output, it must run the consequent or the alternative with the arguments.
5. It must return the output of whichever function is run.

### Coding It

Let's write it out. Using a ternary we can make this a pretty small function.

```jsx
const doIf = (predicate, consequent, alternative) => {
  return (...args) => predicate(...args)
    ? consequent(...args)
    : alternative(...args);
};
```

Now we can compare the code to the requirements to see if they are all satisfied. Take a moment to locate where in the code each requirement is met.

### Variations on a Theme

We could write this out with `if` and `else`. We could assign a variable to store the return value. Or we could shrink it to a one-liner and squeeze out every last character.

```jsx
// Code Golf: Smashed down to 48 characters!
const doIf=(i,t,e)=>(...a)=>(i(...a)?t:e)(...a);
```

If we add variables and code blocks, we have to spend more stripping away the extra bits to follow the logic. If we shrink it down too much it's hard to understand for different reasons. I prefer the readability of our original version. You may see that differently.

Abstraction is one of the great parts of programming. If we know what a function _does_, we don't have to worry about _how_. This is true for built-in functions or ones we write. I haven't read the source code for `Array.prototype.map`, but I know what it _does_, so I can use it confidently.

No matter how we write it, let’s see it in action!

## Examples

Let’s start simple. Say we have a simple function used to get incrementing numbers.

```jsx
const increment = (x) => x + 1;
```

A new requirement is added that we need to return **only even numbers** from `increment`. Seems simple, but we don't know if the numbers passed to us will be even or odd. If we write this imperatively, it might look like this:

```jsx
// Imperative increment
const increment = (x) => {
  // If it's even, add two.  
  if (x % 2 === 0) {
    return x + 2;
  }
  // Must be odd, so add one.
  return x + 1;
};
```

We can write this much smaller, but the same issues happen as before. Compact logic can be harder to read and harder to confirm it does what we expect.

```jsx
// Move the condition to just the added number
const increment = (x) => x + (x % 2 === 0 ? 2 : 1);

// Or use a clever falsy check of the modulo value
const increment = (x) => x + (x % 2 || 2);
```

Let’s see how it could look using `doIf`.

```jsx
const isEven = (x) => x % 2 === 0;
const addOne = (x) => x + 1;
const addTwo = (x) => x + 2;

const increment = doIf(isEven, addTwo, addOne);
```

It has more lines than the short versions of `increment`, but each line here is a small, reusable function, and is almost self-explanatory. It may seem like overkill for a small example but it can make a big difference as conditions and operations become more complex.

```jsx
const getPreferences = doIf(isUserLoggedIn, getUserData, getDefaultPrefs);
```

With small, clearly-named functions, our `doIf` declaration tells the story of if/then/else without anything getting in the way. The conditions and operations are in separate functions, and we are left with *just the logic.*

### Stability and Abstraction

Abstraction makes a big difference to code stability. By breaking down conditions and operations into small pieces to start, we can build up complex code that is resilient to updates. 

Maybe `getUserData` parses some JSON. Maybe it capitalizes some values to make old and new code work together. We don’t have to know anything about those details to understand what our code is doing at a higher level.

Even if we rewrite user management and the code inside `isUserLoggedIn` and `getUserData` changes, this logic can remain the same.

## Add-ons

Now that we have demonstrated the basic functionality we can add some features to make it even better.

### Default Else

Sometimes we don’t need an “else”, so having to specify one is just extra noise. Adding a default is easy enough, but what should the default `alternative` return? We could choose `undefined`, which makes sense for some uses. Functional programming *composition* prefers to return the value passed to us rather than `undefined`, but we will save those details for another time. For now, we can use a helper function called `identity` to give us back the first argument.

```jsx
// Just return what we get
const identity = (value) => value;

// Add a default value to the alternative
const doIf = (predicate, consequent, alternative = identity) => {
  return (...args) => predicate(...args)
    ? consequent(...args)
    : alternative(...args);
};
```

Now we can write our predicate (if) and consequent (then) functions.

```jsx
const isEven = (x) => x % 2 === 0;
const addOne = (x) => x + 1;

const getNearestOdd = doIf(isEven, addOne);

getNearestOdd(10); // 11
getNearestOdd(11); // 11
```

### Not Always Functions

Sometimes we want to return a static value instead of running a function, but we have to wrap that static value in a function for `doIf` Taking the preferences example from before, the supporting code might look a little like this:

```jsx
// A default preference object.
const DEFAULT_PREFS = {
  theme: 'light',
};

// Each step must be a function, so make a function to get our object.
const getDefaultPrefs = () => DEFAULT_PREFS;

// Our function, isolated from the details above.
const getPreferences = doIf(isUserLoggedIn, getUserData, getDefaultPrefs);
```

But can we just pass the static value to `doIf`? It expects functions! Maybe we can do both!

```jsx
// Just return what we get
const identity = (value) => value;

// Return functions or create nullary wrappers for values
const asFunction = (x) => typeof x === 'function' ? x : () => x;

// Add a default value to the alternative
// Wrap the arguments to allow static values, too.
const doIf = (predicate, consequent, alternative = identity) => {
  return (...args) => asFunction(predicate)(...args)
    ? asFunction(consequent)(...args)
    : asFunction(alternative)(...args);
};
```

Rather than re-writing the logic inside `doIf` to check the types and respond differently, we made a small, reusable function that wraps static values so the logic in `doIf` remains simple and easy to follow. `doIf` still only handles functions. Ensuring they are functions is handled by `asFunction`.

`asFunction(predicate)(...args)` might look strange, but we know `asFunction` always returns a function, so we can directly call it. Now we can eliminate the extra function from our example.

```jsx
const DEFAULT_PREFS = {
  theme: 'light',
};

// No intermediate functions between us and the static value 
const getPreferences = doIf(isUserLoggedIn, getUserData, DEFAULT_PREFS);
```

This can also be helpful when we’re performing a number of similar operations.

```jsx
// Needing to pass config to each doIf isn't the best,
//  but that would use utilities we haven't written, yet.
const getProjectStyles = (config) => ({
  ...STYLES_BASE,
  ...doIf(isMobile, STYLES_MOBILE, {})(config),
  ...doIf(isUltraWide, STYLES_WIIIDE, {})(config),
  ...doIf(requestedDarkTheme, STYLES_DARK, STYLES_LIGHT)(config),
  ...doIf(requestedLowMotion, STYLES_LOW_MOTION, STYLES_MOTION)(config),
});
```

Needing to pass config to each doIf isn't the best, so we can map the config argument onto each function, and then merge the results with a reducer. Once again, this breaks the problem into smaller pieces and individual operations.

```jsx
// Collect our conditional style functions
const styleConditions = [
  doIf(isMobile, STYLES_MOBILE, {}),
  doIf(isUltraWide, STYLES_WIIIDE, {}),
  doIf(isDarkTheme, STYLES_DARK, STYLES_LIGHT)
  doIf(requestLowMotion, STYLES_LOW_MOTION, STYLES_MOTION),
]

// Apply the config to all style functions and merge them.
const getProjectStyles = (config) => styleConditions
  .map((fn) => fn(config))
  .reduce((acc, obj) => ({ ...acc, ...obj }), STYLES_BASE);
```

## Summary

I really appreciate how functional programming pushes us to write small functions that can be composed and, most importantly, reused. As you build up libraries of these single-purpose functions, you find yourself *only writing new code*, and just assembling the reusable, reliable pieces that already exist.

Even without helper functions like this, you can – and should! – break up your code. But all that imperative syntax can start to look like *boilerplate* as you get used to the functional style.