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.
- It must accept
predicate
,consequent
andalternative
functions. - It must return a new function. This will accept arguments and run the logic.
- When the new function called, it must run the predicate with the arguments.
- Based on the predicate output, it must run the consequent or the alternative with the arguments.
- 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),
});
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.
// 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.
Top comments (8)
Are you opposed to functional programming in JavaScript, or are there specific things I have done wrong in my implementation?
While an if/then/else function isn't necessarily a core functional programming tool, like curry, it is certainly a part of other FP/FRP libraries in JavaScript, and Haskell's wiki provides arguments for and against an
if'
function.I can add
curry
to my implementation, which would be more consistent with other implementations. It would require eliminating the optional else, so there is a trade-off to be considered.Some examples could be better/more clear. Please let know if you have any feedback.
I wasn't aiming for ad populum with my links to other implementations. You had not yet clarified your perspective that JavaScript cannot be used for functional programming, so I wasn't sure if you were taking issue with something unique to my implementation.
While I appreciate your perspective on pure functional languages, the idea that JavaScript is not capable of functional programming hasn't been accurate for quite some time. As a language it added support for first-class functions back around the turn of the century (Netscape 4.x, as I recall...), and a variety of native methods needed for functional programming have been added since the ES6/ES2015 update.
JavaScript is listed among the many impure functional languages.
While I can appreciate your view that it isn't idiomatic, which is perhaps more true of the
doIf
function than many others, I reject your assertion that "you cannot do functional programing in JavaScript". It is not a purely functional language, but it is capable of functional programming, even if it is not idiomatic.It may not have been idiomatic when the team at Microsoft created RxJS in 2011, or when Lodash implemented an FP-focused version in 2015 (now part of the core Lodash library), and it may not be idiomatic today. But it is functional, however impure and against your liking.
I would recommend Reg Braithwaite's book JavaScript Allongé for a better, more complete presentation on the topic that you may find more agreeable.
I am not trying to stifle discussion, but this comment is repetitive and the parent was deleted, so I am hiding it.
I am happy to continue to engage with you in the thread of your first, more complete message.
This comment is wholly inappropriate, and violates the code of conduct we all agree with.
I do object to the tone of this comment.
The term "impure functional language" came from the Wikipedia entry I referenced. I did not intend for it to be misconstrued as relating to pure/impure functions.
Your definition of functional programming seems very strict and not in keeping with the broader context that has grown over the last fifteen years or so. Functional Programming, its styles, or its techniques, have been applied in many languages.
I understand if you prefer to say this is applying functional programming techniques or using a functional programming style in JavaScript, and I understand if you do not like it or think it is appropriate.
I think the best I can hope for is agreeing to disagree, here.
Where are your texts on "real" functional programming?
Some comments have been hidden by the post's author - find out more