Un-nesting code for less brain-pain

2024 Nov 15  |  6 min read  |  tags: software-engineering 3 art 1

Introduction

"un-nesting": Getting rid of the else statement, making the code flatter.

I don't like thinking too much. So I don't like code that makes me think too much. So, I don't like code that has nested logic in nested if-else statements. I consider nested code as a clear sign of a brain that couldn't linearize the logic well.

When you look at a code with many nested if-else in it, you have to think about all the variations the code flow can take.

"Oh this returns an error here because this if evaluated to False, so it went into this second else that evaluated to True, but this if within that else evaluated to false, so it threw an error"

To me, beautiful code has a very clear execution path, that I can think about linearly.

Instead of doing many nested layers of if-else, just use returns at every step of the way, to escape the flow.

The smallest unit of opening up and flattening an if-else block looks like this:

if condition:
    ...process...
    return bad_scenario
else:
    ...process...
    return good_scenario

Just do

if condition:
    return bad_scenario

return good_scenario

Once this un-nesting pattern is applied recursively in the code's flow many times, across layers of nested if-else(s), you get a very linear code that is very easy to think about.

Some Examples

Here are some examples:

Example 1

Imagine coming across this piece of migraine:

def process_user_data(user):
    if user:
        if user["age"] >= 18:
            if user["subscription"]:
                if user["subscription"]["status"] == "active":
                    return f"User {user['name']} is an active subscriber"
                else:
                    return f"User {user['name']}'s subscription is inactive"
            else:
                return f"User {user['name']} has no subscription"
        else:
            return f"User {user['name']} is underaged"
    else:
        return "User data is missing"

You scratch you head, think up all the paths this code can take through those if-else statements, and finally come up with a list of requirements the code is trying to fulfil:

  • user should be present
  • user's age should be 18 or more
  • user should have a subscription
  • user should have an active subscription

Now, look at this beautiful code, after un-nesting that twisted code from before:

def process_user_data(user):
    if not user:
        return "User data is missing"

    if user["age"] < 18:
        return f"User {user['name']} is underaged"

    if not user.get("subscription"):
        return f"User {user['name']} has no subscription"

    if user["subscription"]["status"] != "active":
        return f"User {user['name']}'s subscription is inactive"

    return f"User {user['name']} is an active subscriber"

Beautiful isn't it? You basically don't even have to think much. It immediately makes sense the first time you read it.

Example 2

Again, imagine coming across this piece of brain hemorrhage:

async def process_data(data: dict):
    if "value" in data:
        if data["value"] > 0:
            transformed_value = data["value"] * 2
            if transformed_value < 100:
                return {"message": "Data processed", "result": transformed_value}
            else:
                raise HTTPException(status_code=400, detail="Result too large")
        else:
            raise HTTPException(status_code=400, detail="Value must be positive")
    else:
        raise HTTPException(status_code=400, detail="Value missing in request")

Again, you scratch your head, and think about all the paths this code can take, and finally come up with what this code is actually tying to do:

  1. value should be present in request
  2. value should be positive
  3. returned value should not exceed 100

Now, look at the un-nested version of that code:

async def process_data(data: dict):
    if "value" not in data:
        raise HTTPException(status_code=400, detail="Value missing in request")

    if data["value"] <= 0:
        raise HTTPException(status_code=400, detail="Value must be positive")

    transformed_value = data["value"] * 2
    if transformed_value >= 100:
        raise HTTPException(status_code=400, detail="Result too large")

    return {"message": "Data processed", "result": transformed_value}

Emergent pattern - Code that screams quickly

When you do this un-nesting enough times, you'll notice a pattern:

  • The un-nested code checks for error conditions and escapes the flow as soon as possible (through returns)
  • Once all the conditions are checked and validated, the code runs the transformations in a single block, and returns results

I call this "code that screams quickly". And I hope that the code you inherited screams quickly.

You'll notice this pattern very clearly when you are implementing validations. As the execution progresses, the inputs progressively become more and more validated, the inputs progressively become more and more correct.

The real skill - Meta-level un-nesting

Un-nesting is obvious when you stare at code within a file. It is easy to do, and is a nice low hanging fruit to pick.

What you need to develop is un-nesting at a higher meta level. Go beyond un-nesting code within a file, to un-nesting the code's flow across files. At this level, you are un-nesting the entire codebase itself.

It is as if you applied the un-nesting algorithm to the entire codebase itself, recursively & bottom up.

Now this takes skill and experience. And it is more art than science. And this is an art that you must develop, otherwise your codebase may rot very very quickly.

Advantages of un-nesting

Un-nested code is very easy to extend. Usually, adding functionality is simply adding another if statement.

When the code is highly nested, you have to think long and hard about where to place the code for the new functionality. Highly nested code is very painful to extend.

When dealing with flat un-nested code, this is very easy. Just add in another if statement somewhere in the chain.

You'll immediately notice this when you are writing APIs. If a new data validation requirement comes up, you just add it as another if statement.

Ending notes

Video you should watch

Youtube: CodeAesthetic: Why You Shouldn't Nest Your Code

Don't go too extreme

There are people who are never-nesters. Never nesting means that there is only one layer of if with the codebase. There is never an if within a if.

I consider this as going extreme, from un-nesting to never-nesting.

You don't have to go this far. You'll routinely encounter code that becomes worse when you do never-nesting.

My advice is simple - "un-nest as much as possible, until the brain pain is gone". Once you achieve this, just stop. Go on a walk or something. Enjoy life.


Now, the next time you come across a codebase that makes you think too much, just think "can un-nesting help?"