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.
In nested code, the happy path is inside. In un-nested code, the happy path is below.
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:
- value should be present in request
- value should be positive
- 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 the (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?"