Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
C, The Beautiful Language (tenaciousc.com)
132 points by pprov on March 14, 2011 | hide | past | favorite | 82 comments


All it means is that he understands C better than other languages. Even Java (yes, Java) has these rhythms that he speaks of. You look at code and you feel the logic, even the necessity of it. That's just the feeling of being in tune with code and with the person who created it, thanks to a shared understanding of the language. Master any language and you'll get the same feeling when you read well-written code. Actually, though, the less boilerplate a language forces you to write, the less you'll feel that "inevitability," since much of the "inevitable" structure of the code -- the predictable patterns in the design of code, or, one might say, "design patterns" -- is code you shouldn't have to write in the first place.


C is the language some may love to hate, but an experienced developer will create clean, concise code. I remember reading through cifsd [1] by cinap_lenrek and thinking, ``my god, this is beautiful''. No boilerplate, just straightforward code. And yes, unobtrusive error handling.

Also, a blogpost post comes to my mind: ``A Timeless, Desert Island Language'': `And that's why I'd choose C as my desert island language. Because in less than 10 pages of C, I can bootstrap a basic Lisp interpreter (...)'.

--

[1] http://plan9.bell-labs.com/sources/contrib/cinap_lenrek/cifs...

[2] http://www.findinglisp.com/blog/2008/06/timeless-desert-isla...


My basic Lisp interpreter was six pages of C, but it's really not usable; it responds to almost any program error (including searching for nonexistent variables) by dumping core. http://lists.canonical.org/pipermail/kragen-hacks/2007-Septe...

Darius Bacon's https://github.com/darius/ichbins is a Lisp-to-C compiler written in itself. It includes an interpreter written in C to bootstrap it — ten pages: https://github.com/darius/ichbins/blob/master/boot-terp/lump... — but the actual compiler is only six pages of Lisp.

My somewhat more performant (but less complete) Scheme-subset-to-assembly compiler is more like 30 pages: http://www.canonical.org/~kragen/sw/urscheme/

If your desert-island language is the one in which bootstrapping your high-level language of choice is the easiest, it seems like you'd probably be better off with Haskell or ML.


>... I can bootstrap a basic Lisp interpreter

That sounds like an argument for any language, though. As long as you can build the language of your choice in language X, you're fine being stuck with X because you're not really stuck.

If they had to write absolutely everything in more "pure" C, would their tune be the same? I think C is a fine language, but it's missing some significant niceties. Like a decent native string class.


> That sounds like an argument for any language, though.

Not really. Unless the language gives you access to the computer (either directly, or by emitting machine code and scheduling for execution), you'll have hard time implementing it efficiently. Compare IronPython (python implemented in python) with the mainline Python (implemented in C).

Yes, there are meta-circular interpreters. But that's not the holy graal.

> I think C is a fine language, but it's missing some significant niceties. Like a decent native string class.

C++ is what happens when you sprinkle a few niceties here and there. The C++ Faq Lite [1] is longer than the whole C specs. The kitchen sink language can do everything, but porting it to a new environment is quite a task. Never mind the compilation time.

You can get an optimizing C compiler [2] in less characters than C++ Faq Lite has.

If you want modernized C with niceties, (Google's) Go probably comes close enough. No string class, thou, just string type.

--

[1] http://www.parashift.com/c++-faq-lite/

[2] http://gsoc.cat-v.org/projects/kencc/


>Not really. Unless the language gives you access to the computer (either directly, or by emitting machine code and scheduling for execution), you'll have hard time implementing it efficiently.

Why not use Ruby to emit a bunch of bytes (ie, assembly), push it into a file, and set the executable bit? Ruby could be used to create GCC so you can write C. ie, a Ruby-based Ruby-to-assembly compiler. With that technique and therefore technically Ruby alone, you're still capable of recreating every language or application in existence, right down to being exact binary duplicates.

Sure it's pedantry. But the entire hypothetical deserted-forever-with-one-language situation is pedantic at its core.


+1 C is ugly.

- Why do I need to typedef a struct to make it look like any other type?

- Why do function pointers look so bad?

- Error handling in C? Those goto exception; if(0){exception:} sure look fantastic.

- int* var1, var2; var2 is an int, not an int* ... The language lets you stick the * right next to the type as though it modifies the type. Which it does in casts. (how do I escape asterisks on here?)

- A system language where int can be 32 bits or more.

And probably more from people more experienced in C than me.


- You don't. C99 treats structs like classes.

- Typedef them.

- If done right, it can be quite unobtrusive.

- That's not how it should be written. This is a 100 times clearer:

int *var1, var2;

- Not really. And don't use int, int32_t is where it's at.


"- That's not how it should be written. This is a 100 times clearer:

int var1, var2;"

Somewhat aside from the actual article, but this might actually be the least clear way to write a definition, since the pointer marker appears to belong to var1, but not the overall definition of int. Here is how I would automatically read the definitions:

"int var1, var2;"

Of type int (variable), create var1, var2.

"int var1, var2;"

Of type int and type * (pointer), create var1, var2.

"int var1, var2;"

Of type int, create pointer var1, pointer var2.

"int *var1, var2;"

Of type int, create pointer var1, default (variable) var2.

Not sure how this works for and against C, since the language definition is really old at this point and by the standards of its time C is really clear. But languages have moved on and a standard has emerged over the intervening time that is a bit more logical.

Also, I think this article is a real piece of fluff :).


    int *var1, var2;
is not clear at all. var1 has type "int pointer", and for each type T, its pointer type is represented as the type T*. It's just one of the many ways the C user interface is broken beyond reason.

The point is not that you can't do good things in C. The point is that it is very easy to do bad things in C. A good UI makes good things the natural way to do things, and actively discourages bad things. C's UI is the opposite.


It's just one of the many ways the C user interface is broken beyond reason.

I'd say this one is just a question of preference. I happen to like to be able to declare my variables together with my ponters.

A good UI makes good things the natural way to do things, and actively discourages bad things. C's UI is the opposite.

In my opinion, that's the beauty of C: unlike Java (and, to a little extent, C++) it doesn't dictate how you write your programs. You are free to shoot yourself in the foot, but you also get to decide exactly how you want your code to look and feel.


The small cost of not being able to declare your variables together with your pointers is nothing compared to the confusion it avoids.

    int* var1;
is the way I (and I suspect anyone who's studied theoretical algebraic data types) think about my variables and types. It sucks that C forces me to mentally translate it to a much less satisfying form.

I'm not sure what being ridiculously easy to make errors in has to do with dictating how to write your programs.


C's designers agree with you about the notation for types and declarations, which was one of the things they fixed when they designed Go.


Well, OK... C is ugly. That doesn't hinder that it is also beautiful.

I did C for ten years, on and off. I stopped. C is an old love where the break up wasn't bad, it was just me that changed.

I realised what I really liked -- to get shit done! That is, to take an idea or a specification and make it exist in the "real" world. That implies the importance of speed, so I can get to the next idea. C just isn't for maximising speed of development, unless for really low level stuff.

Sometimes, I mourn a bit for what could have been.


I can sympathize with the author about C, but with Java... Every time I need to figure out a Java program, I end up being in almost physical pain. Huge trees of empty nested directories and files with nothing but method definitions that do nothing but return a property... or, better yet, tons of empty functions called pure virtual.

It's hard to feel the logic and necessity of code when you can't stop being flabbergasted why the language has to be so verbose and complicated.

I admire Java as an environment, but am scared shitless of it as a language.


But those aren't really problems with Java - they are problems with the idioms common amongst Java developers.


Exactly, and C is the worst offender for boilerplate code. There isn't even a list abstraction! It's surprising that there's an array abstraction and that people aren't just forced to use pointer arithmetic.


This is not fair. C was (and still is) intended for situations where writing your own list abstractions is the right thing to do.

Unlike lists, the array abstraction doesn't cost anything, as it's just straightforward pointer arithmetic underneath. This is why it is in the language.


There are list abstractions in libraries.

See the Linux kernel's list.h for a very nice and generic linked-list implementation.


Occam's razor. Keeps things simple and beautiful.


>In other languages, the abstractions and sweet (if helpful) syntactic sugar that attend the code conceal the heartbeat.

IMO, that means they're failed abstractions and syntactic sugar. Proper ones should result in better understanding due to simplification - you can learn it in levels, easily encapsulating the knowledge at each level so you know you can "trust" what it's doing without having to remember all of it.


As such, any abstraction or syntactic sugar that distances you from the flow of the program fail.

Bu that's for programs that are procedural. Functional programming has no flow. Truth neither moves nor changes.

To use an analogy, a procedural program is like music, where rhythm and motion of the instrument are the essence of what you are representing. Functional programming is more like sculpture, where you create an elaborate piece that has no rhythm or movement of itself, but guides the flow and transformation of the data around it.


I haven't done functional programming really, I didn't think of that case.

Interesting that it still seems to apply, though. If your abstractions in functional programs obscure that flow, they still strike me as a failure. Though I'd be interested in if functional programmers would disagree, I obviously don't know functional design patterns.


If you reason about functional code with appropriate assumptions: they don't lie. A big part of functional programming is making code easier to reason about in isolation.

If you want good procedural C code, read Lua. That shit is polished.


I always thought that part of the reason that c sometime fosters a feeling that "syntatic sugar" is because it doesn't haven nearly as much ability to abstract things as other languages. Although this can be useful to some people in that things may be less context sensitive and more straightforward.


Yeah, code without abstraction is good and works on a small scale. But on a bigger scale, it leads to not being able to see the forest for the trees. I want to be able to see the big picture while being able to dig up the smaller pictures if I have to.


> it leads to not being able to see the forest for the trees

And it's entirely possible the forest will end up being a beautiful one.


> we watch Ronaldo doing things on the pitch

Hmmm...

Lionel Messi does crazy things you never thought were possible. He's Lisp.

Gareth Bale has few fancy tricks, but does what he does very well, and often relies on pure speed - C.

Joey Barton is good, but has a bad reputation and a sordid history - Fortran?

Etc :-)


I wonder which Ronaldo he meant. I can't quite get over the moody, cheating side of Cristiano Ronaldo to appreciate his abilities. Having a Cristiano Ronaldo as a developer would be a bloody nightmare. It'd be like having a co-worker who turned in some inspired code, but who lied, stole, picked fights and was generally a chore to be around.

I hope he meant Brazil's Ronaldo circa 2002 :)


Of course I meant Brazil's Ronaldo...what kind of sports fan do you think I am? :)


Aha the author. I enjoyed your article, but I might be biased as I love football and I quite like C!


... A Real Madrid fan?


You know your soccer my friend ... but you left out Java.

I'd say that's like Miroslav Klose ... big, strong, not particularly fast or lovely to watch but damned good at putting away goals when it matters. surprisingly so.


I like that about Klose :-)

I disagree about Barton - which language consistently repeats past mistakes and is liable to stab you in the eye?


Many people want to leave out Java, that's probably why Klose is currently a substitute.


C is more a description of a Harvard machine than a von Neumann machine. In fact, a useful new language feature would be dynamic generation of machine code.


Of course this isn't a feature of the language -- but most implementations (e.g. gcc) technically allow this, if you really, really know what you're doing. For example:

http://www.cesarbs.org/blog/2010/07/19/run-time-machine-code...


Writing code that writes code, where that written code is executable at machine speeds rather than interpreted, is a qualitative difference over a simple "eval" implementation; and it can most definitely be a feature of the language.

A language which supports it as a feature may do things like partial evaluation of closures, inlining of runtime-constructed code, transforming e.g. the typical block-passing idiom of a Ruby into specific optimized cases. Being able to rely on such transformations in turn lets you build more abstract (i.e. more highly parameterized) libraries because you know you won't have to pay the same degree of abstraction tax.


Yes, you can generate machine code yourself, but then you have to write machine code ...

I'm suggesting a JIT compiler as a built-in language feature so that you could write code in a high-level language and have machine code generated that was specialized based on the program's dynamic state.

It could be done as a library, but it would benefit from language integration so that you wouldn't have to write the code as strings or ASTs.


Language integration would be nice, however dynamic code generation from a library has been about for some time now:

"With libtcc, you can use TCC as a backend for dynamic code generation." from http://bellard.org/tcc/


Lisp provides exactly that: functions are lists (primary data structure of the language) of expressions (in VM's instructions) and you have direct access to VM's memory cells (conses, [1]). You can generate and change functions at runtime. For example, inject tracing code. Actually, at least in some implementations, all functions are created dynamically, except for a few core functions (implemented with C, d'oh).

And how `new' it is? 1958. Oh dear.

Re: ``C is more Harvard than von Neumann architecture''. Hardly; C is architecture agnostic. You can compile it for a Harvard PIC as well as for von Neumann CPU.

It may seem functions are execute-only, but on common CPUs you can treat them as data as well: read and modify (modulo memory protection). And the other way around too -- you can load a bunch of bytes from a DLL, resolve symbols to obtain pointers and use the pointers to execute the loaded bytes as functions.

--

[1] http://en.wikipedia.org/wiki/Cons


Only in early Lisps (and the broken newLisp) are functions lists.

The syntax for functions is lists. But at runtime functions are their own type.


Pardon? [1]

Are you sure? [2]

How about no? [3]

Perhaps you prefer Vim over Emacs? [4]

---

[1] http://en.wikipedia.org/wiki/Picolisp my favorite

[2] http://en.wikipedia.org/wiki/Scheme_(programming_language) probably the best known

[3] http://en.wikipedia.org/wiki/Arc_(programming_language) this bloody website runs on it

[4] http://en.wikipedia.org/wiki/Emacs_Lisp the good, the bad and the ugly, all in one package


In Scheme try and compare

   (car (list 1 2 3 4))
with

   (car (lambda (x) x))
But I guess we are talking about different levels of abstraction?


Not that this has too much to do with what you two were talking about, but (car '(lambda (x) x)) will return lambda in Scheme.


You are right.

Actually I was almost going to add (car '(lambda (x) x)) to my post in the first place; in line with `The syntax for functions is lists.'


Indeed, you are right about Scheme (and Arc). Thanks for the correction.


Sure, no problem. I guess Emacs Lisp falls under the old-and-broken category. And Picolisp eschews functions as a data-type by choice. Do you know whether Picolisp offers lexical scoping?


As the other response says. The FAQ provides some rationale & makes it clear you can, actually, build robust software with picolisp's model of scoping. My understanding is, ``dynamic scoping is faster for an interpreter'' and ``funarg is easy to avoid and closures are possible''.

See the three following points:

http://www.software-lab.de/doc/faq.html#dynamic

http://www.software-lab.de/doc/faq.html#problems

http://www.software-lab.de/doc/faq.html#closures


It offers something similar-but-not-quite called transient symbols: http://www.software-lab.de/doc/faq.html#problems

Alternative phrases for similar-but-not-quite: insidious, error-prone.



It's C++ and of course dependent on Unix/Windows memory mapping APIs, but you might like asmjit: http://code.google.com/p/asmjit/


This is a nicely written tribute, but there are two generalisations that I feel are worth pointing out.

Many parts of Linux kernel are really, really nice C code. Sadly, in my experience most C code is not especially nice. I don't know how this extends to the football metaphor, but certainly I wish most of the C code I have encountered (not to mention written) in my career to date was one tenth as neat.

On balance, I'd say there are other languages (Erlang, Python) where I've personally found the median quality, and level of expression, to be substantially higher (for the random subset of code I've read and my own standards, clearly YMMV.)

The other generalisation is "other languages". Which "other languages" are they? What other projects (despite the Linux kernel) are they reading, and in what contexts?

You can find beautiful or precise code is almost any language if you try hard enough. You can find ugly or obfusticated code in nearly every language without barely trying at all.


What are some specific examples of beautiful C code in your view? Asking for a friend.


After finding the documentation and guides for writing nginx modules woefully inadequate, I did a lot of poking around in the nginx internals to better understand how things worked.

The code is very readable, follows good style guidelines, and has its fair share of brilliant workings. I found that the source code served as much better documentation than a lot of _actual_ documentation that I've read.


You beat me to it! I was just about to suggest the nginx internals. :)


I like poking around FreeBSD's code, specifically some of the drivers and userland. I find a lot of code that rpaulo commits to be particularly clean and understandable. Some of rdivacky's work on boot2 and clangbsd is also inspiring to me.

http://svn.freebsd.org/base/user/rpaulo/


Plan 9 code is beautiful http://plan9.bell-labs.com/sources/plan9/sys/src/

Although there are a few extensions in its C compiler http://plan9.bell-labs.com/sys/doc/compiler.html


Lua, SQLite, the OpenBSD/FreeBSD userland utilities.


The Busybox project.


I'm glad. Somewhere on my mental "code I'd like to read list" is the busybox code, because I don't understand how it does so much with so little. If it does it and looks good, I'm doubly impressed. :)


As the old joke has it, you can write FORTRAN in any language.


In my experience it is more 'fun' to write C code then to lay down yet another line of boiler-plate high level code. However, it seems to take longer to get things done and the resulting code is also much more error-prone (at least my code that is).


I feel C has more boiler-plate code than any other language I've used, including Java--especially when you consider header files, malloc/free, function call error code inspection, etc.


> boiler-plate high level code

If it were truly high-level, this would be an oxymoron.


C was my second love after QBasic. I still admire its simplicity, yet also admire the abstraction, readability, and community of Python.

As anyone on Stack Overflow would tell you, it's a matter of using the right tool for the job. I like to imagine programming language selection as an optimization problem: given a job, over the set of tools, maximize the appropriateness of tools for the job while minimizing the number of tools. Without a tool number constraint, you could call from an infinite number of tools, and one would be best, but that incurs overhead, obviously. The tool number constraint keeps the problem feasible.

For numerical computing, {C, Python} solves my optimization problem.


While we're making silly analogies, let me make one of my own. C is a bit like a horse. It's fun to ride a horse from time to time, and there are still plenty of valid, practical reasons to ride horses. However, you're kidding yourself if you think you're going to talk me into giving up my car (well, ok... my public transportation) for a horse.


C is more like trucks or trains than horses. Backbone of the modern society, but using them for all, or even just day to day, transportation needs could be called misguided.

Fortran is more horsey.


These rhythms reflect a greater underlying symmetry. All languages have them, and some of them reflect these symmetries more easily than others.

When you start to get familiar enough with a language that you see past it to the underlying structure, that's when you really start to appreciate a language. This process is not unique to C, or any other language.


It was a cute sales pitch for the company's C IDE. I especially like how they use "soccer", a word that resembles "sorcery". Talking about how one (only?) gets the gist of things and is not a kernel hacker was great to speak to the less-than-competent. Yes yes few of us write kernel modules but still. Encouraging coders to be comfortable with C when they sell a product to help, brilliant!


Stupid question: what do you do when you're working in C and want OOP?


Use OOP. :)

But seriously, you don't need a language to smack you in the face with OOP syntax to use OOP, and with anything in programming, if you try to do it just like you do in other languages, you're going to be disappointed.


We use GObjects for that: http://library.gnome.org/devel/gobject/stable/

Through GObject Introspection this also means we get automatic OOP bindings to a bunch of higher-level languages


Why would you want that?


http://www.bolthole.com/OO-C-programming.html

That's a simple intro to doing OO in C, mainly by becoming a human compiler that goes from whatever OO language you really want to C.

Note well that OO isn't a well-defined concept. Look at this list of features and ask yourself which of them you think of as being OO:

http://www.paulgraham.com/reesoo.html


Depends on what you mean by OOP. But function pointers (mostly in structs) are one way to do it.


This sort of idiocy is what holds computer science back. C, because of how close to the metal it is, is pretty much the ugliest language in wide use, and the fact that we know a lot more about programming languages and user interfaces now than when C was designed doesn't help. It should only be used as a last resort, when you really need the speed, and even then one should keep an eye out for alternatives.


Which language would you suggest as replacement for writing of sched.c and page_alloc.c?


Oberon or Modula-2 maybe?


What about Ada?


Last resort, remember. I'm not saying C doesn't have its place -- I'm saying that it should be used as little as possible. While I guess there's no other option for a memory allocator, it should be quite possible to write a scheduler in another, better systems language like Rust.

edit: actually, possibly not -- Rust might be a little too abstract. My point still stands, though.


Nice!




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

Search: