The Continuing Relevance of C

This was originally written in early 2001 after a long night of cigars and whiskey with Doug Keester. Any ideas you like in here were probably originally his; I take full responsibility for the turkeys.

In the last few years I’ve seen a great many flamewars over whether or not C is dead, why it is or isn’t, and how much of a justice or travesty it is that it’s dead or not.

I’m hoping that maybe I can shed some light on the subject, giving an explanation of why the unix/C heritage is nothing of which to be ashamed, nor of which to be particularly proud. As well, I’ll be discussing alternatives to the traditional C–oriented worldview to which so many unix hackers succumb.

Theory and Reality

The difference between theory and practice? In theory, there’s no difference between theory and practice. In practice, there is.

— Yogi Berra

Turing, Alonzo Church and the other Knights of the Lambda Calculus were all mathematicians. They came from very strong mathematical backgrounds and did fundamental work in determining the limits of computation — i.e., what problems could be solved and which problems were forever beyond the reach of even the most powerful computers. Even quantum computers are Turing machines; they just happen to be nondeterministic Turing machines, as opposed to the deterministic Turing machines we know and love.

It was probably a great surprise to Turing when he discovered that his wonderful, elegant theories and proofs made exactly a tinker’s dam of difference when the Bombes were malfunctioning or colossus was acting cantankerous. As Maurice Wilkes wrote in 1949, “As soon as we started programming, we found to our surprise that it wasn’t as easy to get programs right as we had thought.”

Computer problems originate in either conception or implementation (which I’ll refer to as “theory” and “practice”). If you don’t understand your problem, you won’t solve your problem. Similarly, if you can’t control your hardware well enough to implement your solution, you’re never going to make your brilliant idea bear fruit.

Solving theoretical problems

Find a better theory. Find a more elegant theory which requires the introduction of fewer concepts, fewer unknowns. Antoine de Saint Exupery, himself an engineer, wrote it best in The Little Prince.

You know you’ve achieved perfection in design, not when you have nothing more to add, but when you have nothing more to take away.

If you’re hitting a brick wall solving a theoretical problem, try programming in a language that has nothing more to take away. These ‘theoretical’ languages exist so that you can take a step away from the dirty, clunky world of gates and transistors, and solve the problem in a manner which is almost transcendent in its elegance.

Over the years, many languages have been developed in order to permit computer scientists to program a computer not according to the base matter of its silicon and substrate, but according to the universal principles of the Turing machine. lisp and Scheme are two fine examples of languages designed to appeal to the theory of computation. A page of well–written lisp code doesn’t just make the solution clear; it gives insight into the mathematical truth by which the problem is being solved. You cannot write effective programs in Prolog, lisp or other similar theoretical languages without first understanding not just how to solve the problem, but why a particular solution works. It is a deeply mind–altering thing to learn theoretical languages, and personally, I think more of us should understand them.

Theoretical languages are not unsuited for life in the Real World. Sometimes, the biggest hurdle in finding a solution is finding the best way to think about the problem. By making the programmer think about the program in terms of abstraction and computational truths, these theoretical languages permit hackers to invent breathtakingly elegant solutions.

When I’m writing Scheme code, I get the feeling that there’s some kind of music being played; and from time to time I’ll actually hear a snippet of something. The answers all come to me in time, slowly unfolding, whispering to me light as the breeze.

Solving practical problems

All this, of course, means jack when you know what the problem is, you know how to solve it, and the damned computer keeps on getting in your way with its silly, arbitrary and aesthetically disgusting rules. lisp has no inherent concept of a 32–bit address space or a 64k memory segment, but the Intel 80x86 architecture sure as hell does.

What to do, then, when the problem is not in your understanding, but rather your inability to break the silicon to your will?

… You get a big hammer.

Practical problems require practical solutions. You need the ability to bit–twiddle at the lowest level of the hardware — because even if you think you won’t ever need the ability to directly address your NE2000 card at 0x300 irq 9, sooner or later you’ll need to do exactly that.

When the rubber meets the road, you want to be the one holding the gun to your computer’s cpu. Practical languages are that gun. Write in Ada95, in C, in C++ — it doesn’t matter, really, anywhere near so much as does the fact that you have Godlike control over the hardware. “You,” you can shout, “I want a full 32–bit far pointer from you, right now, and don’t give me any lip! You! Over there! Yes, you! You’re my new 64k address space. Yeah, buddy, that’s right, this is your six o’clock malloc() call, wake up! And you — what are you doing here? I don’t need you anymore. You’re free() to go, buddy.”

Anyone who doubts that this sort of tyrannical control over hardware is sometimes necessary is someone who has never programmed on any sort of real hardware. There’s a lot of spirited debate over exactly how often this tyranny is necessary, but that’s a much different issue.

When I’m in the middle of a deep C groove, I can almost hear Maxwell’s demons screaming in agony as they flip the transistors inside my CPU, begging, pleading for mercy. The output of my C code is a gift made by millions of subservient, recalcitrant malcontents, an offering to the crazed god who demands of them “flip this” and “set that”.

unix: theory with practice

Every operating system has to run on real hardware at some point, and this is where we return to unix and C. unix is by nature a crossplatform operating system; it runs on Big Iron, it runs on Little Tin, it runs in the embedded space, it runs in the rtos space, it runs … everywhere. The Space Shuttle has taken Debian into orbit at least once, and it’s a unix which handles life support on the International Space Station.

Saying that unix supports diverse hardware is understating things.

This means there are enormous regions of the problem set which do not merely suggest C, they flat–out require C (or another language which gives equivalent fine–grain control). No–one in their right mind would suggest writing a mission–critical device driver in Ocaml, but nobody thinks twice about writing mission–critical device drivers in Assembly or Ada. The Space Shuttle’s avionics software is written in hal/s, an obnoxiously difficult programming language with excellent control of hardware. It’s never, not once, had a life–endangering failure.

Most of the credit for the reliability of the Shuttle’s code goes to the programming crew, who are by all accounts professionals of the highest caliber. The choice of hal/s as a language is indicative of something, though — if these professionals thought they could safely do away with worrying about machine details, or they could safely do away with the ability to exert incredibly fine–grained control over the hardware, I suspect they’d do it in a heartbeat. Assembly is hard on the brain and unforgiving of even the slightest errors.

Kind of like C, matter of fact.

When the rubber meets the road — or the drive head scans across the platter — you absolutely must be able to control the computer’s operations in either realtime or a reasonably close approximation of it. This is where “practical languages” like C come into their own. For hardcore systems programming, there are no other realistic options.

But at the same time, if you don’t need the fine–grained control or the blazing speed of a “practical language”, you’re probably better off using something (anything!) else. Languages which give you that much control over your hardware also give you that much ability to shoot yourself in the foot. When you screw up in a “practical language”, the consequences are almost uniformly dire.

This doesn’t necessarily mean that whenever you’re not operating under serious time constraints or control requirements you should break out the lisp, by the way. My theoretical/practical distinction is much clearer than exists in reality. In theory, the theory is right… in practice, it’s not. There are well–earned niches for shell languages, for scripting tools like Perl and Python, for ecmascript and on and on.

Conclusion

At the beginning, I said that I hoped to show why there was nothing in the unix/C heritage of which to be ashamed, nor of which to be particularly proud. Now that I’ve given exposition on what I feel to be the true heritage of unix — namely, the marriage of theory with practice — let me clearly explain the thesis.

The unix/C heritage is nothing of which to be ashamed because, realistically, what other options exist? In what other language should the kernel and supporting libraries be written? While it’s unargued that C is a language fraught with peril for the newcomer, it allows the fine–grained control and blazing speed which are required for a high–performance system. In essence, C solves exactly the problem for which it was designed — and there’s absolutely no reason to be ashamed of something which does its job well.

In a similar vein, the unix/C heritage is nothing of which to be particularly proud, either. The kernel and libraries could be written in Assembler, for all anyone cares, or Ada95 — and as long as the APIs provided were functionally identical, few users would even notice. It could be unix/intercal for all I care; the magic of unix is found not in a computer language, but in its design.

There is a drawback, and not an insignificant one, to unix’s strong C heritage. Namely, many newcomers to the unix world have the understandable (though mistaken) belief that C is somehow the talismanic ne plus ultra of the software world. It’s not, nor was it ever. Many young hackers believe that C is the only language worth programming in. It’s not, and for many tasks, much better languages exist.

That drawback is not so large as to consign C to irrelevancy. Today, and for the forseeable future, C will continue to be a useful, relevant part of most every unix.

Email me.