Creating a Functional elseIf Utility Function
@superlucky84|August 7, 2024 (7m ago)400 views

- Table of Contents
- Triggering Event
- Reckless Implementation
- A More Refined Implementation
- So, What's the Benefit?
Triggering Event
I wasn't entirely unfamiliar with functional programming, but after randomly coming across the book Functional Programming Made Clear, I felt inspired to apply functional programming more actively to my code.
There is a utility called pipe that aids functional programming by composing multiple functions together using commas, creating a new function. This makes it much easier to track the flow of inputs and outputs across multiple functions.
Whenever I came across books or materials on functional programming, I often found myself unconsciously wondering what more I needed to reach a level where I could freely apply pipe
to real-world code in my company's work.
Example of Using
pipe
const f = R.pipe(Math.pow, R.negate, R.inc);
f(3, 4); // -(3^4) + 1
One leisurely day off, I turned on my computer early in the morning to play a game. But out of nowhere, my curiosity about functional programming, which had been lingering in my subconscious, suddenly spoke to me:
"How do you connect if else
logic within pipe
?"
I figured I should resolve this curiosity first before I could fully enjoy my game.
After browsing through well-known functional programming libraries, I quickly found utilities that provided functionality similar to what I was looking for: ramdaJs(ifElse) and fxJs(ifElse). Below is an example of how ifElse
is used in RamdaJs.
const incCount = R.ifElse(
R.has('count'),
R.over(R.lensProp('count'), R.inc),
R.assoc('count', 1)
);
pipe(
incCount(), //=> Result { count: 2 }
...
...
)({ count: 1 })
If you want to chain another ifElse
operation to this code, you need to do it as shown below.
const incCount = R.ifElse(
R.has('count'),
R.over(R.lensProp('count'), R.inc),
R.ifElse(
R.has('chk'),
R.assoc('count', 3),
R.assoc('count', 1)
);
);
Reckless Implementation
It would be nice if everything could be expressed cleanly in a single depth, but as I add more if else
conditions, the depth keeps increasing, which I don’t like.
To get straight to the point—I did eventually find another utility function that perfectly matched the interface I wanted. However, I found it too late, and in the meantime, I ended up creating something weird on my own.
The interface of the strange utility function I made looks like this:
The first argument of elseIf
is a predicate, and the second argument is the return value when the predicate evaluates to true
.
const getGrade = pipe(
elseIf(
value => value === 1,
() => 'Gold Member'
),
elseIf(
value => value === 2,
() => 'Silver Member'
),
elseIf(
value => value === 3,
() => 'Regular Member'
),
elseEnd(() => 'Others')
);
chkGrade(2); // Silver Member
The key concept behind implementing this is that if the predicate of an elseIf
condition earlier in the chain evaluates to true
, the subsequent elseIf
conditions are no longer needed. Once a value is determined, the later elseIf
functions should simply pass the value through without processing it further.
The solution I found was to wrap the determined value in an object called Decision
. Then, when the other elseIf
conditions later in the chain encounter a value wrapped in Decision
, they just pass it through. Below is the code illustrating this approach.
const elseIf = curry(
(is, run, value) =>
value instanceof Decision
? value // If the value is already a Decision object, return it as is
: is(value)
? Decision.of(run(value)) // If the predicate is true and the value is not a Decision, wrap the result in a Decision object
: value // If the predicate is false, return the original value
);
This way, you can keep chaining functions in a single depth, and it will be handled properly.
The reason I wrote it this way is that while reading books on functional programming, I came across the concept of "monads." Monads are special objects that wrap values, not using them directly, but protecting them. When unexpected values are encountered in the values that the function processes, the object wrapping the value ensures that the value flows smoothly through the pipeline without any issues until the very end.
One of the most common examples used to explain monads is the Maybe Monad. I also implemented elseIf
using hints I got from studying the Maybe Monad. While what I implemented isn't an exact technical implementation of a monad, I wanted to avoid large amounts of code and quickly move my thoughts into a minimal code form, so this is how it ended up. I implemented it by just checking the type of the object wrapping the value and passing it along.
After the last elseIf
, you connect elseEnd
to extract the value wrapped in the Decision
object and return it.
const elseEnd = curry((run, value) =>
value instanceof Decision ? value.get() : run(value)
);
A More Refined Implementation
After implementing it (which, honestly, was just opening a file and quickly testing to see if it worked as I had imagined), I started wondering whether this could be practically used. I was doing the dishes at home, thinking about it. After researching and pondering for a while, I realized the morning had flown by, which made me a bit frustrated. But no matter how much I thought about it, something felt wrong or missing, so I decided to look for utility functions implemented by other libraries.
And sure enough, I found the utility I was looking for under the name cond
. It exists in libraries like ramdaJs(cond), remedaJs(cond), and fxJs(cond). Below is how you can use it.
In the nested array, the first element is the predicate inside the if
statement, and the second element corresponds to the body of the if
scope that is executed when the predicate is true.
Example of using
cond
in RamdaJs
const fn = R.cond([
[R.equals(0), R.always('water freezes at 0°C')],
[R.equals(100), R.always('water boils at 100°C')],
[R.T, temp => 'nothing special happens at ' + temp + '°C'],
]);
fn(0); //=> 'water freezes at 0°C'
fn(50); //=> 'nothing special happens at 50°C'
fn(100); //=> 'water boils at 100°C'
If I had discovered this from the beginning, I wouldn't have wasted half a day pondering over it and could have just focused on the game, which is a bit disappointing. Still, to wrap up my thoughts neatly, I decided to implement cond
using the elseIf
and elseEnd
functions I made.
You just need to convert the shape of showcases([if: () => boolean, run: () => any][])
into the shape of IelseIf[]
, like the example below.
Implementation
const cond = curry(condition => {
const condPipe = pipe(
map(([arg1, arg2]) => (arg2 ? elseIf(arg1, arg2) : elseEnd(arg1))),
arr => [...arr, elseEnd(value => value)]
)(condition);
return pipe(...condPipe);
// The code above generates something like this:
// pipe(
// ...[
// elseIf(arg1, arg2),
// elseIf(arg1, arg2),
// elseIf(arg1, arg2),
// ... // repeat for the number of conditions
// elseEnd(value => value),
// ]
// )
});
It's so simple that there's not much to explain. You can use it like the example below. Anyway, it's a bit frustrating that I wasted half a precious holiday on this.
const chkGrade = cond([
[value => value === 1, () => 'Gold Member'],
[value => value === 2, () => 'Silver Member'],
[value => value === 3, () => 'Regular Member'],
[() => 'Others'],
]);
chkGrade(2); // Silver Member
So, What's the Benefit?
I felt frustrated about wasting half a day on a random thought that came out of nowhere, and I started questioning whether it was worth spending half of my holiday on this. I wondered if it was a useful thought process that had any real value.
One of the common advantages of functional programming, as explained in the books I read, is that when you turn things like arithmetic operators (+, -) or control structures (such as if
statements) into functions—things that aren't first-class citizens (i.e., not treated as values)—they become first-class, making the code more flexible and allowing for more dynamic and adaptable solutions.
For example, if you want to add functionality to display "Others" for any non-Gold members in the chkGrade
function from the previous example, you can easily handle this by adding an elseIf
clause like this:
Example of using
elseIf
const chkGrade = (value, chkOnlyVip) => {
return pipe(
...[
elseIf(
value => value === 1,
() => 'Gold Member'
),
!chkOnlyVip &&
elseIf(
value => value === 2,
() => 'Silver Member'
),
!chkOnlyVip &&
elseIf(
value => value === 3,
() => 'Regular Member'
),
elseEnd(() => 'Others'),
].filter(item => item)
)(value);
};
chkGrade(2, true); // Others
Example of using
cond
const chkGrade = (value, chkOnlyVip) => {
return cond(
[
[value => value === 1, () => 'Gold Member'],
!chkOnlyVip && [value => value === 2, () => 'Silver Member'],
!chkOnlyVip && [value => value === 3, () => 'Regular Member'],
[() => 'Others'],
].filter(item => item)
)(value);
};
Anyway, it was a fun time pondering and exploring different ideas. In the end, I realized that others are using something similar to what I had in mind, which reassured me that my approach to functional programming wasn't completely off-track.
And while how useful it is certainly matters, I think if I enjoyed the process, that’s enough for me.