The video is titled «Why You Shouldn’t Nest Your Code», but it hardly answers this question at all – I count 1 sentence. A better title would have been «How To Reduce Indentation In Your Code».
Here is the «four deep» example from the video:
int calculate(int bottom, int top)
{
if (top > bottom)
{
int sum = 0;
for (int number = bottom; number <= top; number++)
{
if (number % 2 == 0)
{
sum += number;
}
}
return sum;
}
else
{
return 0;
}
}
Here is it again, but reformatted:
int calculate(int bottom, int top) {
if (top > bottom) {
int sum = 0;
for (int number = bottom; number <= top; number++) {
if (number % 2 == 0) { sum += number; } }
return sum;
} else {
return 0;
}
}
Now is it still so bad?
To be clear: I do agree that the transformations proposed by the video are improvements.
Note that there is a slight of hand at 1:55: the ‘extracted’ code actually isn’t present in the original code. Instead, first the code is implicitly transformed
// from
if (number % 2 == 0) { sum += number; }
// into
if (number % 2 == 0) { addend = number; } else { addend = 0; }
sum += addend;
and only then can the if
be extracted.
( Actually, there are more steps: from addend
to number % 2 == 0 ? number : 0
and then back again to if
. )
Erratum: the condition (top > bottom)
‘flipped’ is not (top < bottom)
but rather (top <= bottom)
. Indeed, reordering logic is often not trivial and CONSTANT VIGILANCE is appropriate.
I tend to dislike the effects of extraction as demonstrated in the video. In many languages it forces code apart. If possible I’d like to keep it local.
Compare
// original
pub fn is_leap_year(year: u64) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
// divisibility check extracted
fn divisible_by(year: u64, d: u64) -> bool {
year % d == 0
}
pub fn is_leap_year(year: u64) -> bool {
divisible_by(year, 4) && (
!divisible_by(year, 100) || divisible_by(year, 400)
)
}
// but I'd rather have (and Rust lets me 🎉)
pub fn is_leap_year(year: u64) -> bool {
let divisible_by = |d| year % d == 0;
divisible_by(4) && (!divisible_by(100) || divisible_by(400))
}
Languages differ in refactorizability. I hate Python in this regard. Rust seems decent. But Haskell is great at it.
Most languages lack where
clauses and let
expressions. I miss them regularly.
A `where` demo
This particular example doesn’t really let where
shine, but maybe it gets its point across?
-- Analogous to the original C code, so it looks weird
calculate :: Int -> Int -> Int
calculate bottom top = runST $ do
if top > bottom
then do
sum <- newSTRef 0
for_ [bottom .. top] $ \number -> do
when (even number) $ do
modifySTRef' sum (+ number)
readSTRef sum
else pure 0
-- and now with subexpressions extracted into a `where` clause:
calculate :: Int -> Int -> Int
calculate bottom top = runST $ do
if top <= bottom
then pure 0
else addUpTheEvens
where
addUpTheEvens = do
sum <- newSTRef 0
for_ [bottom .. top] (whenEvenAddTo sum)
readSTRef sum
whenEvenAddTo sum number = modifySTRef' sum (+ filterNumber number)
filterNumber number = if even number then number else 0
-- lest you think Haskell is crazy, here is an idiomatic solution
calculate :: Int -> Int -> Int
calculate bottom top
| top > bottom = sum (filter even [bottom .. top])
| otherwise = 0
Now, to answer
I’m not sure. I do tend to avoid deep indentation, but not because of the indentation. I never think I want to reduce indentation, but reduction of indentation regularly is an effect of my efforts to make the code more intelligible.