What is simple code? Everyone has a different definition but I think we can draw some universal distinctions. I was thinking of this the other day when I read a nice LtU post about complexity. It had a couple of semantically equivalent code examples. I'm going to pull them over here to make a point.
Here's low simplicity (in C++):
float *miles_to_kilometers(float *mile_list, int length) {
float *kilo_list = new float[length];
for(int i = 0; i < length; i++) {
kilo_list[i] = mile_list[i] * 1.61;
}
return kilo_list;
}
Higher simplicity (in Perl):
sub miles_to_kilometers {
my @result = map { $_ * 1.61 } @{$_[0]};
return @result;
}
And extremely high simplicity (in Haskell):
milesToKilometers = map (* 1.61)
Low simplicity is characterized by the use of widely understood constructs. A first year CS student would have no trouble understanding the C++ example.
The Perl example is uses a higher level abstraction, map, so it's relatively opaque if you don't know what a map is, but once you do, you can appreciate the simplicity.
The Haskell version.. well what can I say about Haskell? It's a profoundly elegant language but it's opaque if you don't have the requisite understanding.
The Haskell version uses a functional language style called "point free" programming. It defines a function called milesToKilometers that accepts a list and applies map to it with an anonymous function that multiplies each element by 1.6. If you're wondering where the list is in the definition, don't. It's not there. The Haskell type system is advanced enough to infer types for you, so you can leave off the list argument. The compiler knows that milesToKilometers can accept a list because map accepts one.
Bewildering? Yes, but it's also simple -- once you understand it.
That's the difference between low simplicity and high simplicity -- you have to work to understand high simplicity. But, high simplicity isn't defined just by the amount of work you have to do. If it was, all of the crufty code that we've ever scratched our heads over would be simple. We know that's not the case.
You have high simplicity when you can take something away from that work.. something that makes the next thing you encounter easier to understand. The map pattern is one of these things, and point-free programming is another. Languages will differ in their support for these constructions but there's a lot of value in knowing them, regardless of what language you use. High simplicity is concise, and it rides on top of simple well-defined abstractions.
"A first year CS student would have no trouble understanding the C++ example."
Because we taught them to understand imperative programs!
If we taught them to understand functional programs in the first year then we would expect them to have no trouble understanding the Haskell example.
"At Oxford, we use Oberon as the second language we teach to our undergraduate students (the first one is Haskell)."
http://spivey.oriel.ox.ac.uk/mike/obc/
Presumably those first year students would have not trouble understanding your Haskell example.
Posted by: Isaac Gouy | January 10, 2007 at 01:47 PM
"Presumably those first year students would have not trouble understanding your Haskell example."
I agree. I think that it is hard to have a good handle on simplicity when we aren't conscious of what we are acclimated to. Foreign language text looks absolutely incomprehensible if you don't know the language, but clear and lucid if you do.
I've been playing with Haskell a bit lately, and liking it. I'm now going through the inevitable process of mapping it back to my OO experience. It feels odd.
Posted by: Michael Feathers | January 10, 2007 at 02:16 PM
Preface: I like Haskel quite a lot, and my defense of Perl is not an attack thereon.
However: why make the Perl example artificially complex? If it were written as:
sub miles_to_kilometers { map {$_ * 1.61} @_ }
then it would look an awful lot more like the Haskel example (setting aside Perl's much-maligned punctuation, it still would highlite the ability for Haskel to omit explicit arguments, but show parity in other respects).
It's pedantic, I know, and in an example such as this, Haskel *should* still appear simpler (because it is). It's just a pet peeve that whenever two languages are compared by example, one example always tends to better-written and one worse-written, each in the context of its own language. Also, for that matter, example problems are generally *chosen* for the purpose of highliting one language over another.
If the example, for instance, had involved I/O, I doubt that Haskel would have had a chance at appearing "simple". Even by your standard of "simple -- once you understand it" (which I both like and dislike), monads fair poorly, relative to imperative I/O.
Posted by: Steven Stone | January 10, 2007 at 02:54 PM
"However: why make the Perl example artificially complex?"
Apologies on that. I didn't write the examples (and it wouldn't have helped, I've learned and forgotten Perl three times so far in my life), I pulled the examples from the LtU post. I just thought there was an interesting point to make there and it's more about the abstractions than the language.
On another site, someone pointed out that the C++ example could be made a lot simpler with std::transform. I agree.
Posted by: Michael Feathers | January 10, 2007 at 03:02 PM
"... hard to have a good handle on simplicity when we aren't conscious of what we are acclimated to."
Now if we can remember back decades, those who've done any high-school maths will already have used functions, and those who've done a little more high-school maths will already have used function composition.
In contrast, iterative indexed assignment is a wierd new concept.
"... now going through the inevitable process of mapping it back to my OO experience"
Of course you're using something that supports both functional and OO programming, something like Nice or Scala ;-)
Posted by: Isaac Gouy | January 10, 2007 at 03:50 PM
"Of course you're using something that supports both functional and OO programming, something like Nice or Scala ;-)"
Nah, Haskell. Cold turkey. :-) I played with OCaml a few years ago, but I then decided it would be good to try a pure functional language.
Posted by: Michael Feathers | January 10, 2007 at 06:27 PM
"it would be good to try a pure functional language"
Sure enough, but how are you going to 'map it back to your OO experience' without having a straightforward way to do both fp and oop?
I found having both oop and fp in Mozart/Oz really simplified re-writing programs from one style to another, and I think using oop and fp in Scala would also provide an easy way to switch from one style to another, without also having to mentally adjust syntax.
Posted by: Isaac Gouy | January 10, 2007 at 06:53 PM
Thanks for the nice post, and I agree to the comment that said a particular implementation is more easily understood by freshmen due to the students' historical reasons; the perception between low & high simplicity would be significantly affected by the cultural background and context of the person. However, as Christopher Alexander explores in his Nature of Order series, I think there could be some portion of absolute beauty, and simplicity that is universal, considering we are physically more or less same.
Oh, BTW, J (http://jsoftware.com) is even simpler than Haskell's.
m2k=.*&1.61
In J's world it happens very often that the name(identifier) is longer than the referent(code). So the referent becomes a name itself(idiom).
+/%# is the code for average.
Posted by: June Kim | January 10, 2007 at 07:07 PM
While I know that this doesnt really have a lot to do with your point, the first example you posted was in C, not in C++.
The first example in C++.
void miles_to_kilometers(const vector &mile_list, vector &kilo_list_out)
{
transform(mile_list.begin(), mile_list.end(), back_inserter(kilo_list_out), bind1st(multiplies(), 1.61));
}
Posted by: Jonathan | January 10, 2007 at 07:28 PM
FYI, between the perl and haskell versions, lies the ruby version:
def miles_to_kilometers(miles)
miles.map {|mile| mile * 1.61}
end
which is IMO, much less "magical" than the perl equivalent, e.g. explicit params instead of $_
Oh, and we could use the "pre" tag in the comments, btw...
Posted by: Ben W. | January 10, 2007 at 11:25 PM
Your C++ contains a memory leak. Not that important for the purpose of this article, but it is a bad example for those who do not know C++.
Posted by: Ziggy Stardust | January 11, 2007 at 01:55 AM
I think you have an unnecessary 's' in your theme. I think you're talking about high 'implicity'. . The C++ example is pretty explicit, but it's also not very simple. The Haskell example at the end is very small, but requires you (and the runtime) to know things that aren't being said explicitly.
I think that it is simpler, too, in a hard measure of the number of operators, entities, side-effects and syntactic elements involved in the declaration. In fact, simplicity is pretty much the inverse of that count. maybe (operators + entities + syntax) * sideEffects. ;-)
It's just funny that part of the low count for haskell is that some parts of it are implicit and elsewhere.
I think that a moderate implicity and a high simplicity are good things, though I follow the old Python adage that explicit is better than implicit.
Posted by: tim | January 11, 2007 at 10:45 AM
June: Thanks! I'll have to try J.
Jonathan: Fair enough, it could've used STL, but it's definitely C++ code. I like C++, but one of the things I've noticed traveling around is that there are far people working in C++ that looks like that than there are avid users of STL and Boost. I guess that long tail of history is success, in a way.
Ziggy: No memory leak. It's just a function written with the assumption that the caller will own the result. Note to self: stop defending the examples, Mike. You didn't even write them(!)
Tim: Yeah, I struggle with the implicitness in Haskell, but I wonder again how much of it is just what we're used to? Dynamically typed languages are inherently more implicit than manifest statically typed languages like Java and C++. Neat metric, btw. It would give most Haskell code a rank of zero. :-)
Posted by: Michael Feathers | January 11, 2007 at 11:34 AM