Any Never Nesters out there?

Yeah, me too. Kind of. :wink:

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.

“Hiding” one level of nesting by formatting the code differently doesn’t feel like a real alternative to me. I mean I could remove all indentation from the code or put everything on one line but that surely wouldn’t make the code easier to read.

For these kinds of discussion my goto book is “Code Complete 2” by Steve McConnell. It discusses all these low-level things in depth and at length, from how to name variables and functions, when and code should be commented (and when not), etc.
He also discusses nesting and says that “studies by Noam Chomsky and Gerald Weinberg suggest that few people can understand more than three levels of nested ifs” (he cites Edward Yourdon: “Managing the Structured Techniques: Strategies for Software Development in the 1990s” for that), and that “many researchers recommend avoiding nesting to more than three or four levels” (he cites Glenford J. Myers: “Software Reliability”, David Marca: “Some Pascal Style Guidelines”, and Henry Ledgard & John Tauer: “C With Excellence: Programming Proverbs”).

1 Like

I agree it does not make the logic easier to understand. Still, I do find it easier to see the logic in the reformatted version. My point being – also made by you but from the other end – that formatting contributes to understanding as well. In particular, excessive whitespace by itself can worsen intelligibility, even apart from ‘nesting’.

1 Like

I’ve watched the video as well a while ago, and another channel made a “spiritual successor” video which I liked even more.

The central premise is that going from imperative / procedural to functional / declarative style can be a more powerful refactoring tool to make the code more readable than rearranging the order of if-statements and for-loops.

We can subordinate detail if we let go of holding onto classical loop structures. Let’s unpack the brevity of a raku approach (a raku track is available on Exercism):

sub calculate ( $bottom, $top ) {

    sum grep * %% 2, $bottom .. $top

}

The three nests in the original are for:

  1. A divisibility test: raku has a divisibility operator %% which forgoes this nest while also addressing the common and problematic workaround of checking a remainder for zero.

  2. A classic for loop for iteration: raku prefers functional and concurrent iterating constructs which often subordinate this level of detail. Here a range operator .. compacts this nest.

  3. A coherence check that top > bottom: fence post error aside, we can forgo this check as the sum of nothing is zero. This leans on the notion of multiple values for false from the empty set to the empty string.

geekandpoke200843simply-explained-part-14.html-min

I’m sure many take this as argument for using Rust for new or rewriting projects. But the point is, you have to leave your environment and even your style.

It’s quite nice to see, that you can make your code clearer - and idealy more robust - without leaving the environment. That’s usually just given, but the way you write code can make the difference.

You don’t.

I strongly suspect you can totally write code very similar to that Rust or Haskell code in C or Go. The key is to choose the right abstraction. In this case it’s iterators.

This is more like what the original video suggests:

int sum(int bottom, int top) {
    int sum = 0;

    for (int number = bottom; number <= top; number++)
    {
        if (number % 2 == 0)
        {
            sum += number;
        }
    }

    return sum;
}

int calculate(int bottom, int top)
{
    if (top <= bottom)
    {
        return 0;
    }

    return sum(bottom, top);
}

I strongly suspect you can totally write code very similar to that Rust or Haskell code in C or Go.

Yes, the original can be expressed almost verbatim in raku too:

sub calculate ( Int $bottom, Int $top ) {
    if $top >= $bottom {
        my Int $sum = 0;
        loop ( my Int $number = $bottom; $number <= $top; $number++ ) {
            if $number % 2 == 0 {
                $sum += $number;
            }
        }
        return $sum;
    } else {
        return 0;
    }
}

This is sometimes referred to as baby raku. It is valid and I’d argue has a place in the scheme of things to get you started !

The verbose way would usually be possible. But the Rust way is not possible with Go and C. Just because the language doesn’t offer those features.

So no, you can’t write those Rust style code with any language.

Which features?

Most by far of the involved machinery is defined using Rust code. The rest is just syntactic sugar. Similarly, the Haskell version uses only syntactic sugar and laziness – which Rust doesn’t even have.

Go just doesn’t have the way to work with iterators like Rust.

It is possible, just not very ergonomic (maybe my unfamiliarity with Go): Go Playground - The Go Programming Language

Numbers := InclusiveRange{0, 10}.Iter()
Evens := Filter[int](&Numbers, func(n *int) bool {
	return *n%2 == 0
})
fmt.Println(Sum(&Evens))

It’s technically possible, but a bit pointless in C without polymorphism/generics. You’d have to either:

  • handroll everything for every type
  • get lost in macro hell
  • do some sort of funny type erasure

Well, sure you can imitate. None of this is part of Go itself. At which that’s the case for Rust. And since it’s not part, it’s just putting some pattern into a language it’s not made for.

Thanks for your Playground. You see how many code you had to write, to end with a solution which feels so wrong for Go.

The problem is not that iterators are not “part of the language”. In Rust, iterators are not part of the language either. They are entirely implemented in the standard library.

The main function in @pshen 's playground is all that would be needed if the Go standard library included all the things that Rust does. And the main function is pretty much the same as the Rust version. Superficial syntax differences aside.

Generics are essential to that playground though. C still doesn’t have them and probably never will. And the entire Go ecosystem has evolved without them, which is why it “feels wrong” in this language. It is unfamiliar to the eye of a trained Go developer.

OK, fair point. stdlib is for me part of the language ecosystem. For sure not 100% accurate perspective. The point is even, that Go does not has any compiler optimizations like Rust for that.

While I like to learn from different languages new approaches, it always is important to stay in the track and don’t invent artificially patterns on top of everything. You can find many discussions about this Go and iterator topic.

Back to the original topic: It’s possible to optimize structure in any language and respect the patterns that are normally used there. It’s not that pattern X from language Y should be solution for language Z.

Hehehe I managed to write even more unholy-Go to reproduce the method chaining syntax. Here’s the modified playground @deleted-user-33691 how much do you hate this? :smiling_imp:

Sadly, I didn’t manage to apply method chaining syntax to Sum(). Apparently, Go’s generics do not allow methods on generic structs with instantiated type arguments. Too bad.

Jokes aside though, I have to agree with @deleted-user-33691. Language design puts limitations on what kind of patterns can be used to produce readable, maintainable code. Best not stray too far from the beaten path.

BTW, there is already a quite nice clone of lodash for Go (samber/lo: :boom: A Lodash-style Go library based on Go 1.18+ Generics (map, filter, contains, find…) (github.com)). Didn’t check in detail. But some of the discussed points - if not even all - are addressed there.

I guess chaining is out of its scope at all.

Not that I don’t like those chainings. :sunglasses: Is quite powerful once you got it. Using it with JavaScript and TypeScript for a long time. And also like those aspects in Rust. On the otherhand, I like the clear - and more verbose way - of C and Go. Gives another form of power of your code.

I’m totally amazed what the Rust compiler does with those chains. There were some tests of one student here, that it not even is a difference of using multiple filters or make one filter with multiple conditions. For Go those optimizations are just not intended. So you mostly optimize by writing different code.