doIf (if/then/else): JavaScript Functional Programming

doif-(if/then/else):-javascript-functional-programming

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 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.

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.

// 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.

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:

// 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.

// 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.

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.

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.

// 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.

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:

// 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!

// 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.

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.

// 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),
});

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.

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
how-to-add-text-similarity-to-your-applications-easily-using-mediapipe-and-python

How to add text similarity to your applications easily using MediaPipe and Python

Next Post
13-content-marketing-trends-you-need-to-follow

13 Content Marketing Trends You Need to Follow

Related Posts