Hacker News new | past | comments | ask | show | jobs | submit login
Tom Duff: Reading Code From Top to Bottom (1999) (iq0.com)
59 points by gnosis on June 5, 2011 | hide | past | favorite | 22 comments



Duff's advice on avoiding if/else is echoed in a functional programming context by CMU's Bob Harper, who writes, "[A]void if-then-else entirely, and instead use only case analysis for branching..." http://existentialtype.wordpress.com/2011/04/17/some-advice-...


Uhh, not same reasons. Harper is an ML hacker, he wants to avoid IFs to encourage generic programming and pattern matching. Adding a new branch to an if clause requires some nesting, adding a branch to a case/cond is just one entry of many. After that, pattern matching becomes natural an you tend to see a function not as a single wholesome "body", but a family of mappings.

Again, switch, duff's device & other computed-goto hacks are "constructs", a chunk of "decision" code that sits in the middle of the page directing traffic. Pattern-matching is far more sublime than that: along with manifesting itself as a visual construct, it can also hide in the head of the function, cozzying up with its parameters, and also in binding/assignment forms.

A distant, but weaker cousin of pattern-matching is destructuring, more common in Lisps.


Can someone older or wiser than me discuss what the idea is behind "only one return per method" in imperative code? I agree completely that it makes many methods much more difficult to read, and I've never understood why it was taught as a guideline in the past (and why some people still seem to subscribe to it today.)

EDIT: It makes a lot more sense to me now that I think about it in the context of a language like C, with no garbage collection and no exceptions. Thanks!


Makes maintenance simpler; any resource cleanup can go through one point and you can see that it is correct when modifications are made. (C++ gives this to you with destructors, mostly).

Also makes it easier to step through code ("I'll just set a breakpoint and see what this function returns...").

I usually advocate using early returns for near boilerplate things like parameter checking, then a single return that does cleanup and exit, with 'goto' encouraged as the proxy for a return-in-the-middle. I think of it as similar to what a C++ compiler does for you.

These days I code for maintenance, since someone /is/ going to go in and start adding early returns and subsequent bugs. Plan for code rot.


It was part of the structured programming dogma. One entry point, one exit point. Multiple return was lumped in with using goto.

I never use goto, but I often use multiple returns similar to the examples in this article.

I have sadly code reviewed many pieces of code with the nesting problem.


if you have to use C++ without exceptions (console video games), then goto is much needed to handle exceptional situations locally. For example there would be label "fail:;" or "out:;" at the end of the function, where release of resource would happen, and you would "goto" to it, rather than releasing the resources in multiple exceptional places and return from there.


Interesting. I rarely program in an environment without exceptions, so I haven't had reason to adopt that style.

I recall that Linus has advocated the use of goto in the Linux kernel for similar reasons. And the goto version of this code does seem more readable to me: http://kerneltrap.org/node/553/2131


If you are using C++ (even without exceptions), then RAII (Resource Acquisition Is Initialization) with resources freed in local variables' destructors is a better solution.


Why don't console games use exceptions?


No compiler support on one common system.

You should be handling every case anyway - by the time the code is done, there are no exceptional situations left ;)


Far easier to do stuff like 'assert(postcondition)'


I found a few other interesting little notes on his site:

http://iq0.com/notes/trig.html All you need to know about Trignometry

http://iq0.com/notes/frobenius.html The Frobenius-Burnside Counting Formula

http://iq0.com/notes/geometric.html Sum of a Geometric Series


On the topic of loops which end in a break ("Most egregious is the loop whose last statement is break;!"): this can be useful for avoiding contorted control flow when you have a switch statement inside the loop. Instead of coding:

    cont = 1;
    while (cont)
    {
        foo = bar();
        switch (foo)
        {
            case 0:
                cont = 0;
                break;
            default:
                // other work
                break;
        }
    }
You can do this instead:

    for (;;)
    {
        foo = bar();
        switch (foo)
        {
            case 0:
                break;
            default:
                // other work
                continue;
        }
        break;
    }
IMO the idiom, once you're used to it, reads better.

(Yes, I know you can reduce the lines further e.g. putting foo=bar() into the for statement, but it's the multi-level break I'm focused on here.)


Why not:

    while( (foo = bar()) != 0 )
    {
        /* other work */
    }
Or even simpler (since 'foo' serves no real purpose in the example):

    while( bar() )
    {
        /* other work */
    }


One, very simple, example:

int some_function( some_variable ) {

  if (some_variable) {
    b = 1;
  } else {
    b = 2;
  }

  log("Its important I log that I'm returning %d\n", b);

  return b;
}

If its not obvious why this is superior...


Go's defer feature, combined with named return values, lets you do this:

    func someFunction(b bool) (i int) {
        defer func() {
            log.Printf("I'm returning %d", i)
        }()
        if b {
            return 1
        }
        return 0
    }
For more on defer, see this blog post: http://blog.golang.org/2010/08/defer-panic-and-recover.html


The single return is also useful for post-condition assertions of return values and internal state.


This of course exemplifies why it's impossible to declare almost any technique categorically "good" or "bad". Sometimes early returns make code shorter and more clear. Sometimes the opposite. To borrow from Stroustrup, experience and taste matter a lot.


In dynamic languages you can do this:

  some_function = function() {
    if (...) return 1;
    if (...) return 2;
  }.log("It's important I log that I'm returning");


  int some_function( some_variable ) {

    log("Its important I log that I'm returning %d\n", (some_variable ? 1 : 2));

    return (some_variable ? 1 : 2);
  }


Now you're duplicating logic for a log statement...


I have a new _no_ computation in log statements rule. A bug in there is nigh near impossible to fix because you're not looking for it.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: