Seal the Exits: Rethinking the If/Else Statement

agustus_gloop

Sean Travis Taylor

Posted on September 3, 2024

Seal the Exits: Rethinking the If/Else Statement

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));

Enter fullscreen mode Exit fullscreen mode

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
  }
*/
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
agustus_gloop
Sean Travis Taylor

Posted on September 3, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

What was your win this week?
weeklyretro What was your win this week?

November 29, 2024

Where GitOps Meets ClickOps
devops Where GitOps Meets ClickOps

November 29, 2024

How to Use KitOps with MLflow
beginners How to Use KitOps with MLflow

November 29, 2024

Modern C++ for LeetCode 🧑‍💻🚀
leetcode Modern C++ for LeetCode 🧑‍💻🚀

November 29, 2024