Seal the Exits: Rethinking the If/Else Statement
Sean Travis Taylor
Posted on September 3, 2024
If you gathered anything from the last two posts (here and here) you have detected a common theme–eliminating branching code paths.
Why?
Programs are the assembly of logical and computational pathways; this is how they work. A program without branches wouldn’t accomplish anything interesting.
This is true; the program is the product of its logic paths. However, while your interpreter traverses any number of branching conditionals, the human brain is less adept.
The human eye and mind requires some consistency of structure, some stability of visual design before the code can be reasoned about. It is toward making code reasonable that we aim our effort at eliminating branches. Reasonable code is easier to debug, enhance and modify, to discuss and to understand.
The reward for pruning code branches is conferred upon the reader of the code, rarely the author. Yet it’s worth considering alternatives to traditional imperative approaches of expressing conditional logic when such efforts increase the effectiveness of our programs.
Recall a gnarly thicket of if/else
statements from a codebase you know. It is a pain to look at and to understand. Such code includes a self-preservation mechanism: any sensible programmer won’t touch code they cannot comprehend. So it is that troublesome code survives.
function evaluateLoanApplication(applicant) {
if (applicant.creditScore >= 700) {
if (applicant.income > 50000) {
if (applicant.employmentStatus === 'Employed') {
if (applicant.debtToIncomeRatio < 0.3) {
return "Loan Approved";
} else {
return "Loan Rejected: Debt-to-income ratio too high";
}
} else if (applicant.employmentStatus === 'Self-Employed') {
if (applicant.yearsInBusiness > 2) {
return "Loan Approved";
} else {
return "Loan Rejected: Self-employed less than 2 years";
}
} else {
return "Loan Rejected: Employment status not verified";
}
} else {
return "Loan Rejected: Income too low";
}
} else {
return "Loan Rejected: Credit score too low";
}
}
const applicant = {
creditScore: 720,
income: 55000,
employmentStatus: 'Employed',
debtToIncomeRatio: 0.28,
yearsInBusiness: 0 // Not relevant for employed
};
console.log(evaluateLoanApplication(applicant));
Bet you've never seen anything like this before...
Each if/else
statement is an escape hatch for our program's flow to evade us. They are open windows. We must monitor them, manage them and mitigate their negative effects on our code as lost control flow is routinely a source of bugs.
If you read previous posts, you may await a proposal to eliminate code branches accompanied by if/else
blocks.
You are correct!
The next pattern draws inspiration from a focus of the previous posts: returning values from functions. “What do functions have to do with if/else statements?” you may wonder. Further: “How can an if statement return anything?”
May we present a new type: the if/either
.
Understanding this type depends on drawing a distinction between statements and expressions.
Statements define actions or declare variables. They control the flow of execution and encompass conditionals, loops and methods that return nothing.
Expressions however, return values; they can combine with other expressions for composable program flows.
By converting formerly separate branches in our if/else
blocks into expressions, we receive values resulting from the condition’s evaluation.
The actual branching is encapsulated within the if/either
type. We only have to decide what should happen in our code.
Since a value is returned from evaluating the condition, we never lose the program flow. Our execution path remains unchanged. There are no errant branches to cause headaches later. The branching is abstracted; there is nothing for us to neglect.
// `Result` and `If` type definitions omitted for brevity
// We convert our conditionals to function expressions
/**
* @returns {Result}
*/
function checkCreditScore(applicant) {
return If(applicant.creditScore >= 700).either(
() => Result.ok(applicant),
() => Result.error('Credit score too low')
);
}
/**
* @returns {Result}
*/
function checkIncome(applicant) {
return If(applicant.income > 50000).either(
() => Result.ok(applicant),
() => Result.error('Income too low')
);
}
/**
* @returns {Result}
*/
function checkEmploymentStatus(applicant) {
return If(applicant.employmentStatus === 'Employed').either(
() => Result.ok(applicant),
() => Result.error('Employment status not verified')
);
}
/**
* @returns {Result}
*/
function checkDebtRatio(applicant) {
return If(applicant.debtToIncomeRatio < 0.3).either(
() => Result.ok('Loan Approved'),
() => Result.error('Debt-to-income ratio too high')
);
}
/**
* Evaluates the loan application using chained maps
* @returns {Result}
*/
function evaluateLoanApplication(applicant) {
return checkCreditScore(applicant)
.map(checkIncome)
.map(checkEmploymentStatus)
.map(checkDebtRatio);
}
// Example usage
const applicant = {
creditScore: 720,
income: 75000, // This will trigger an error
employmentStatus: 'Employed',
debtToIncomeRatio: 0.28,
};
const applicationResult = evaluateLoanApplication(applicant);
// Post-processing. We could go on all day here with maps all the way down..
const processedResult = applicationResult.map((result) => {
return `Your next step is to finalize the loan agreement.`;
});
console.log(applicationResult); // To see the direct outcome:
/*
Result {
ok: <Boolean>,
value: 'Loan Approved'| null,
error: <String> | null
}
*/
// If the evaluation process meets all criteria:
console.log(processedResult);
/*
Result {
ok: <Boolean>,
value: 'Your next step is to finalize the loan agreement.' | null ,
error: <String> | null
}
*/
An expression evaluating to a Boolean, a method call to .either
, a function to execute if the condition is true and another to execute if false. This is a simple API. Yet this simplicity conceals the complexity of branching code paths while making them more manageable.
The if/either
takes further inspiration from the Either monad in functional programming. As always, you must decide how far down the functional rabbit hole to venture: whether to experiment with your own implementation or to use suitable libraries. The approach illustrated here is a naive introduction to a powerful strategy in program design.
Further, when paired with our Result
type as shown above, we get succinct, composable, expressive code. The execution flow is clear and crisply manicured code paths allow for no means for our program to escape.
Posted on September 3, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.