Hacker News new | past | comments | ask | show | jobs | submit login
The Wrong Aesthetic (sheddingbikes.com)
101 points by urlwolf on June 13, 2010 | hide | past | favorite | 70 comments



I've always felt that most good programmers go through 3 stages.

Stage 1: Write very simple, naive code, no fancy design patterns, just kind of brute force everything.

Stage 2: Discover design pattens, and fancy obscure programming constructs. Use them everywhere regardless of whether it makes sense or makes the code easier to understand and maintain.

Stage 3: Realize the folly of stage 2, and enter a zen like state where one writes deceptively simple, clean code. Rarely, if ever, use any fancy constructs or design patterns (except where it actually makes sense to use them.)

For the novice programmer looking at someone else's code it's very very easy to confuse stages 1 and 3.


There is a similar trajectory in what you choose to build. In stage 1 you write programs that are conspicuously missing features; in stage 2 you write programs that have too many features (and yet may still be missing some important ones); in stage 3 you write programs that do exactly what's needed.

My experience suggests that one good way to speed the transition to stage 3 is to try to make your programs short. When I was writing On Lisp I spent a lot of time working on programs that had to be short enough to be reproduced in the book, and this cured me of the tendency to pile on features.

Rtm's example probably helped too. His source code is as laconic as he is in person.


Irish trad musicians go through similar stages:

Stage 1: Learning, perfecting skills with simpler music.

Stage 2: Discovering new techniques and musical ideas, stretching their skills, attempting to pull off dense and complex ornamentation as fast as possible.

Stage 3: Realizing that stage 2 may make for a good show, but can impede great music. Enter a zen like state where one plays deceptively simple but tasteful, clear, and highly effective expressions of fundamental musical ideas. (EDIT: Which enhance a given tune as a coherent whole.)

For the novice player, it's possible to mistake stage 3 for stage 1 and common to conclude the progression stops at stage 2.


Great observation. I'd say this applies to all musicians.


I think there is a stage between 2 and 3 where people become obsessed with coding standards that say thing like "don't use the ternary operator because it can be confusing".


Wasn't aware that this was a consensus; I always avoid ternary expressions for this reason.


The syntax for ternary expressions in Python seems pretty hard to confuse:

    x if condition else y


It's uglier in Java:

minVal = a < b ? a : b;

I never use them, and typically enforce coding standards preventing their use as well. Good code should be easy to read, and if/else blocks are easy to read.


I find inline Java ternaries often make the code easier to read overall than a whole if-else block, for two reasons:

1. The code is shorter, so there's less to read in the first place.

2. The value returned is being used right where it's needed, so you don't have to carry it so far in your head.

I certainly don't use them all the time though, just for short/simple expressions.


Lets better hope we never end up on the same team then. I have no patience for dumbing down shit to people who have no business writing code.

The ternary operator is a standard and it is in the language for a good reason - it can make certain statements smaller and involve simpler code (that is, less places to screw up).

Yes it can be abused, but so can everything else. The solution is not to cut out features but to cut out those who are not fit to program.


The use of ternary operators is a standard idiom, at least in C, and reduces the visual noise of if..then..else used to implement code like your example.

Enforcing coding standards to ban the use of standard idiom is exactly the sort of reason that I refuse to work for bone-headed cubicle farm-type companies.


The use of the ternary operator for such things as: String a = (b == null) ? "" : b;

is okay with me, at least, when I'm working in languages other than C#, where it's just: string a = b ?? "";

I also use it within other operations, like: var output = string.Format("Something {0} something {1}", string1.Length > 0 ? string1 : "N/A", string2.Length > 0 ? string2 : "whoops");

My general rule of thumb is, if breaking the ternary operation into multiple lines will make it more readable, then you're going too far with the ternary operator.


As ugly as the ternary ?: operator is, it's an expression so that makes it nicer than if/else blocks that require variable mutation.


To be honest, this surprises me a lot. I'm not a professional programmer, for some definition of same; but I do write a great deal of code and make my living from using and maintaining it.

And for me, saying

foo = bar<baz ? 42 : 0;

is really quite nice.


Even stage 2 programmer confuse stage 3 for stage 1, this makes very difficult to help stage 2 programmer grow. I suppose this is the difference between knowing the path and walking the path ;)

I find my way through TDD, but all my tentatives to help others failed. I can understand why because all tentatives to help me before I see the light on my own failed equality. I can remember some conversations with more experienced programmers where I tried to convince them about the benefits of more design as they were trying to explain me the vertues of simplicity...


This applies to every domain which has experts. Somehow, the learning curve takes you places you later abandon.

If there is a lesson to take from this is that one might be able to get a feeling for what the extra fluff in second stage looks like. The hype, the enjoyment of doing things for the wrong reasons etc.

A rather insidious side effect is that most people who teach / talk about the process are in second stage. The experts usually went beyond examining the process, and have little interest in discussing it.


Being a novice programmer, what should I make of this?


You are going to make a lot of mistakes, some painful, others interesting. Happy hacking!


If you're lucky someone may mistake you for a zen master


I guess this is what he meant to write, programming language permitting:

    int Total()
    {
        return dice.Sum(die => die.FaceValue);
    }


It should be pointed out, because I fear people are missing this point, that while the instantiation of his point is language-specific, the general point holds regardless. In Haskell, to take the opposite extreme, "foldl (+) (map dieSum dies) 0" would be idiomatic (or would be if the sum function wasn't built in to Prelude as well but I feel that's cheating too much), whereas the closest equivalent to the loop approach in C++ would be grotesque and unidiomatic. How grotesque would be up to the user; you could go all the way to having the die values in an array and iterating an index through them if you like, but it'll make the C++ version of that look pretty sane.

The exact algorithms aren't the point. It's really something more like "stay idiomatic in the language you are in and keep it simple". Another example is when people start going nuts with lambda in Python; you're really not supposed to do that and there is almost always a better way that doesn't involve lambdas.


Yes, staying idiomatic is a good idea, it's worth pointing out explicitly.

As a tangent, while the original author was trying to abstract away the loop, I feel he left too many fragments of the loop behind, such as the start and end conditions. As such, I don't think the sum function is cheating too much, and is in fact the goal. I might describe the Total as "the sum of the dies' face values," instead of the more verbose: "the sum of the dies' face values, starting from the first die and ending at the last die."

In a sense, this runs parallel to your reminder to stay idiomatic: when you sum a list of things, it is common to add them all together, not just some subset. It is this shared understanding which lets us be succinct.

Though I do fear that I'm preaching to the choir :-)


The problem is that there is more than one idiomatic c++


Clojure: (reduce + (map face-value dice))


Let's not start a language pissing contest on HN of all places, please.


That wasn't my intention. I enjoy seeing idiomatic solutions to problems in different languages and thought others might, too.


Python:

  sum(d.faceValue() for d in dice)


Or:

( loop for die in dice sum ( face-value die ))

Been a while since I've done any lisp, but pretty sure that is how you would probably do it.


Could also do something like: (reduce + (map face-value dice))


what Lisp-1 is that? look clean.


It's scheme, but with a custom version of reduce that doesn't take the identity parameter. When working with lists that are known to be non-empty, it's a convenient shortcut.


What language is this?


It looks like C# 3+.



or, in APL/K style languages,

+/dice`facevalue


I disagree. I like the second example, because it explains what it's trying to do -- accumulate, over +, the values of the dice from begin to end. Instead of dumbing down the algorithm for the machine, it uses the words in the problem domain to describe the problem. (Now, you could argue that C++ is messy because you are using a hammer to screw in a screw, and that's true. You could also argue that it's stupid to rewrite code in this new way when the old way works fine and the problem is so simple. Also a valid point.)

Oh, and I don't know Boost, but I guessed that the error was a missing _ before the 1. And after reading the linked article, it turned out that I was right. So it's not that hard to figure out.

I think the article makes sense if you consider its source. This is Zed Shaw, who has given up high-level languages in favor of C, at least for the purpose of blog posts. Of course he would prefer the "how the machine does it" version of the code to "how the programmer thinks of it" version that Boost (and presumably Ruby) prefer.

I think 50 years of programming has proven that approach wrong, but he is entitled to believe whatever he wants to believe. I take the opposite stance -- describe your problem as precisely as possible, and let an optimized library or language figure out how to get the computer to solve the problem quickly. You can do both, of course, but keep the two parts separated!


Great Post!

While a lot of factors go into determining whether a language is readable I have always felt the most obvious is familiarity. The human mind is very good at adaptation, and often it’s astonishing what we will perceive as "normal." Familiarity only comes from constant exposure, though, which means that languages with relatively simple syntax become familiar more quickly. Lisp is at one extreme, with only one syntactic construct. It’s very easy to become familiar with Lisp, although grasping the large Common Lisp standard library is another matter. I tend to agree with the author that C++ is a language at the other extreme. Most C++ coders I have encountered use only a relatively small subset of the C++ language. Worse yet, everyone uses a slightly different subset.

Of course, the biggest impact on readability comes not from the language, but from the developer. A poor developer can write illegible code in any language. A good developer? I’ve even seen well-written, readable Visual Basic code (once).


Such functional code in C++ doesn't need to be bad. Though I would have coded it a bit different:

    int Dice::total() const {
        return sum(dice.begin(), dice.end(), _1 ->* &Dice::faceValue());
    }
with:

    template<typename T, typename Iter>
    T sum(Iter begin, Iter end, function<T (Iter)> f = *_1) {
        if(begin != end)
            return sum(next(begin), end, f) + f(begin));
        return 0;
   }
This allows some more flexibility. For example, you could also provide other implementations of sum. You could also easily make your code threaded without changing the code of Dice::total (well, take this example a bit carefully -- of course it implies that faceValue() is threadsafe and also does only make sense if faceValue is expensive to call).

Summing up some numbers may be anyway not the best example for this because the naive implementation is really so short and trivial and in most cases enough.


Tail call optimization is not guaranteed for optimized C/C++ programs, and is not done at all in debug mode, so this kind of implementation can cause trouble with large structures.

I used to have fun, too, implementing tail-recursive functions to do stuff like flashing a LED on microcontrollers. I have since recovered :)

It is also my experience that helper functions like "sum" tend to be used very few times, and need a lot of variations, so it's actually better to just write the loop when needed. The theoritically advantageous functional programming is hard to convey into C++. Syntax and other technical reasons are partly to blame, but the biggest has to be simply the culture. Very few C++ coders think functionally, and coding, especially in C++, tends to be a team job...


The tail recursion was not my point. I also could have given another implementation of sum.

I wanted to demonstrate that the implementation of Dice::total() can be much clearer and still in a similar functional way.


I dunno. This reminds me of that post about "Say what you mean in javascript"

http://news.ycombinator.com/item?id=1358753

Sometimes, things are confusing or are seemingly super weird depending on your background. While I haven't done C++ in years, I recognized that std::accumulate() was probably very much like Ruby's inject().

I suspect those that have seen map, inject, and their ilk wouldn't be so confused.

While you can argue that the 'average corporate programmer' wouldn't know what the hell it is, and you might be right. But if we stuck with that attitude, we'd still be using goto's liberally because the 'average corporate programmer' would find for loops weird and confusing.


> I suspect those that have seen map, inject, and their ilk wouldn't be so confused.

No doubt. However, C++ stays a major contributor to that confusion. In ML languages,

    let total = foldl (+) 1 (map face_value dice)
is nearly idiomatic. Compare that to the C++ code. Compare that to C++ code that would use accumulate and an equivalent of map.


I don't disagree. But it's a weakness of C++, not of the language concept itself.

Perhaps if OP was saying you shouldn't use it for C++, then maybe I'd say he'd have a case.


> Perhaps if OP was saying you shouldn't use it for C++

I think that's exactly what he was saying.


It is.


Ah. I didn't get that from reading it.


And in Haskell,

  foldl (+) 1 (map face_value dice)
would use fusion to avoid causing map to create another array!


Incidentally fusion won't work for this example (at least not as currently implemented.) Only `foldr` is fusable. Fortunately, regular inlining does just fine.

There is of course an even simpler version:

    sum (map face_value dice)


Why does the sum start with 1? Why not eg.

     foldl1' (+) $ map face_value dice


It should have started with 0. Sorry. And I avoided `foldl1` because I wanted to stay close to C++'s `accumulate`.


Shouldn't that be:

let total = foldl (+) 0 (map face_value dice)


Whoops. Yes it should.


I don't particularly understand what makes the second one more confusing than the first.

In particular, as a non-c programmer this: "(*i)->faceValue()" would be pretty confusing to me.

The first one in 'English':

"For each i starting at dice.begin(), while i is not equal to dice.end, incrementing i. Add to 'total' the face value of the thing that i is pointing to."

The second one in 'English': "Accumulate from dice.begin to dice.end, initializing the accumulator to 0, adding the face value of each die."

The first one might be idiomatic C++, but it doesn't seem to me that it would be idiomatic in any natural language.

(reduce #'+ (map 'list #'faceValue dice))

what is obvious depends on your background.


Really?

bind(std::plus(), _1, bind(&Die::faceValue, _2))

reads as "adding the face value of each die." to you? how. Because not knowing bind() or c++ I'm wondering what _1 and _2 means. More importantly why bind seems to take an operator, _#, and something, on the outside but takes only an integer and _# on the inside call. and, although the surround code might explain it, where 'Die' came from.

Unless you're just overlooking everything on that line except Die::facevalue. If you are, then why keep mentioning the 'i' in the for loop version.


Yes, really!

I do a lot more functional programming than I do imperative programming. An anonymous function in context of an accumulator is obvious to me.

The point that I maybe didn't make so well was that the second version is working in higher level constructs. The first version is doing a lot of integer and pointer manipulation, the second is summing across a collection.

The two versions might be speaking in different languages, to some extent, but that doesn't mean that one or the other is aesthetically wrong.


"I don't particularly understand what makes the second one more confusing than the first."

To understand the first one, you only need to know the basic C++ constructs. For the second one you need to know:

* the meaning of the four positional arguments to accumulate()

* the semantics of bind()

* the semantics of boost's placeholders

Even with some knowledge of the above, figuring out the nested binds takes a while.


Wish zed would have a date on the blogpost of when he posted that.


``import time; print time.ctime(1276445247)`` where 1276445247 is the post's filename.


Ah, I see, thanks for that.


This would have been fine with a BOOST_FOREACH. I find it hard to believe that he knows his way around boost:bind, but not BOOST_FOREACH.


Take one look at the code and dependencies behind BOOST_FOREACH, and you'll thunk twice before using it.


I have taken a look (and read the article about it). I'm not sure I agree that this should stop you using it. Its like saying you shouldn't use Rails because there is deep magic going on.


Did anyone else notice the inconsistency without Zed's help?

Even if I hadn't, it still wouldn't have affected how I thought about the code. It's just a type error that would have been caught by the compiler.

http://www.boost.org/doc/libs/1_43_0/boost/bind/placeholders...


So in other words the author isn't used to boost::bind and stl algorithms and complains that he doesn't understand code written with both?

First of all today you would rather use a lambda which is easier to understand.

The advantage of the second code is that it's generic and less error prone. You can't get the loop wrong. You can change the addition by any operation very easily, and the compiler will complain when you get things wrong.

fyi, the same code written with a lambda:

    int Dice::total() const {
      return std::accumulate(
          dice.begin(),
          dice.end(),
          0,
          [&](size_t v, const Die & d) -> size_t { return v + d.faceValue(); });
      );
    }
For more information about why the second version is better, I advise the book "Elements of programming".

ps: the spelling error introduced by the author prevents the code from compiling, so the argument doesn't hold


errr, no.

It is in no way more generic or less error prone. Just asserting that does not make it so.

You _can_ get the loop wrong, if you accumulate(dice1.begin(), dice2.end(), ...) for example, or switching the begin() and the end(). You can just as easily change the addition operation to anything else in the non-boost/stl version.

In the 2nd version, when you _do_ make a mistake, if the compiler catches at you get an error message that's totally unreadable with 2000 character error lines. And if the compiler doesn't catch it, and it causes a runtime problem, it is also extremely awkward to backtrace.

Not to mention, you need the latest-and-greatest compiler to do the lambda thingy, for no obvious benefit.


Most STL implementations catch the two errors you gave in debug mode (switching begin and end, using two different containers, etc.), and many more!

As for the compiler giving crazy errors, it's true for g++ mainly. VC10 and most of all clang give intelligible messages.


I haven't been following STL implementations recently, but that definitely wasn't the situation even in 2007 (Over 10 years after STL was finalized..); In fact, the only implementation that it did back then was STLPort, at a huge performance hit, and only at runtime.

And what you say about compilers is EXACTLY why I wrote the "latest and greatest". It's only recent clangs that can give intelligible messages for C++ (if at all) because Clang C++ support is not even ready for production yet.

VC++ 2005 (last I used) did not get give reasonable errors.


The argument does hold. Some code doesn't compile immediately. Also, Zed's code is understandable by even a programming novice. Your example strikes me as gratuitous adulation of the abstruse.


the spelling error was intentional. see the end of the article.


I suspect that some of the programmers who find the second version better are non-experienced. To them, the first version is not standard C++ code, so it looks as "ugly" as the second version. The second version is cooler (= harder to understand), so they prefer it.

Having said that, the second version written in a better language is pretty standard. It's mostly the limits of C++ which make the "functional" version look so bad, imo.




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

Search: