12 ms·
John Carmack's comment on Doom 3's code style
- jlongster 14y agoMy heart grew a little warm with the last paragraph of John Carmack's comment: "The major evolution that is still going on for me is towards a more functional programming style, which involves unlearning a lot of old habits, and backing away from some OOP directions."
- zaphoyd 14y agoHas anyone here come across any particularly good books or other resources on functional style programming in C++? Especially ones that have been updated for C++11s new functional features.
- gits1225 14y agoBartosz Milewski's blog: http://bartoszmilewski.com/ http://bartoszmilewski.com/
- quux 14y agoJohn Carmack wrote a nice article about experiences writing functional code in C++ here: http://www.altdevblogaday.com/2012/04/26/functional-programming-in-c/ http://www.altdevblogaday.com/2012/04/26/functional-programm...
- mark-r 14y agoThat very link was at the bottom of his original response.
- quux 14y agoAh, didn't notice it. Thanks. If I missed it, I can't be the only one.
- worldsayshi 14y ago"a function can still be pure even if it calls impure functions, as long as the side effects don’t escape the outer function" This is a very good point that probably could be systematically exploited. Does anyone know examples of this?
- greiskul 14y agoI think caching/memoization is one of the most typical cases of this.
- ufo 14y agoIn Haskell there is the ST monad can be used to write stateful implementations for pure functions. The type system guarantees that side effects can't escape their scope. http://www.haskell.org/haskellwiki/Monad/ST http://www.haskell.org/haskellwiki/Monad/ST
- worldsayshi 14y agoAlthough this is great information I primarily had 'less pure' languages' in mind when I asked. Come to think of it, restricting mutable access to only the relevant objects as per function call using const seems to allow just this! A function having only const input and output "should be" free of side effects (if no global mutables); while still being allowed to run all manner of unpure processes local to its own calling context.
- mattdw 14y agoClojure has "transient" (mutable) forms of its immutable datastructures, which can be bashed in place, but they're not allowed beyond function boundaries in either direction (I think). So you convert (copy?) to transient, mutate it in-place, then convert back to "persistent" before returning it.
- crntaylor 14y agoHere's a pretty trivial example in Haskell - computing the factorial function using a mutable variable. import Control.Monad.ST import Data.STRef fact :: Int -> Int fact n = runST (fact' n) fact' :: Int -> ST s Int fact' n = do a <- newSTRef 1 mapM_ (\x -> modifySTRef a (*x)) [1..n] readSTRef a Here the function `fact'` uses mutable variables (encoded in the use of `ST` in its type -- `ST` stands for State Thread) but the function `fact` is pure -- the call to `runST` ensures that none of the side effects leak out of `fact'`. As with most Haskell code, the types are optional - I included them for clarity.
- agumonkey 14y agoTim Sweeney of Epic also praised functional programming few years ago http://www.st.cs.uni-saarland.de/edu/seminare/2005/advanced-fp/docs/sweeny.pdf http://www.st.cs.uni-saarland.de/edu/seminare/2005/advanced-... I don't know what's his stance on the subject since though.
- AlexDanger 14y agoHe uses C# to provide examples of dynamic failure in compiler checked code. Since then, some of his 'what we want to write' pseudo-code examples are now reality in C#.
- Cowen 14y agoHe's mentioned before that he's toyed with the idea of switching to Haskell. That wasn't for functional programming though. It was instead for the built-in static code analysis the Haskell type system and compiler provides.
- graue 14y agoDo you have a link to the source for that? I'm curious to read. If your description is accurate, Rust[1] is exactly what he wants. I've been learning Rust recently (with a little help from [2]) and it does a ton of static code analysis, is safe by default (no dangling or null pointers, no shared mutable state), uses Hindley-Milner type interface just like Haskell, and generally is what you would expect if Haskell and C++ had a baby. [1] http://www.rust-lang.org/ http://www.rust-lang.org/ [2] http://www.rustforrubyists.com/ http://www.rustforrubyists.com/
- gnuvince 14y agoRust is pretty much exactly what a lot of people want and need; a high level language with a strong focus on safety and concurrency, while still allowing you to control the lower level details such as memory layout for good performance.
- ansible 14y agoI'll say this for them, they know how to write an introductory home page. All too often, I've heard about some language, and then spent minutes trying to figure out what it is all about. Rust is definitely worth a longer look by me.
- gtani 14y agoHe sort of alluded to it, but there's the usual perceived tooling/ retraining/hiring / performance ceiling issues for shops with > N devs, N somewhere between 5 and 25 (I don't agree with his arguments, just repeating them) http://gamasutra.com/view/news/169296/Indepth_Functional_programming_in_C.php http://gamasutra.com/view/news/169296/Indepth_Functional_pro... http://www.reddit.com/r/haskell/comments/jap3x/im_very_tempted_to_want_to_move_to_a_functional/ http://www.reddit.com/r/haskell/comments/jap3x/im_very_tempt... http://blogs.uw.edu/ajko/2012/08/22/john-carmack-discusses-the-art-and-science-of-software-engineering/ http://blogs.uw.edu/ajko/2012/08/22/john-carmack-discusses-t...
- melling 14y ago"I am a full const nazi nowadays, and I chide any programmer that doesn’t const every variable and parameter that can be." Immutability...one less thing to worry about.
- ajross 14y agoYes, but it's easy to spend way too much time chasing that stuff, too. The API you're working with may not itself be const-clean, so you end up with a zillion const_cast expressions translating your "cleanly consted" local expressions into something the compiler will accept. And all that junk hurts maintainability and readability; you end up being tempted into nonsense like "caching" your non-const handle just to avoid all the casts, etc... Broadly, my feeling is that getting your own APIs (internal and external) to be const correct is important and worth the trouble. But don't jump through hoops to shoehorn the strategy into someone else's code where it isn't honored.
- aardvark179 14y agoI think that highlights a broader point that is touched on by the article. The Doom 3 code is not idiomatic C++ the way many would think of it but it does build its own consistent idioms and sticks with them wherever reasonable. It can do this through having a fairly small number of points where it touches the external world and ensuring that its idioms are compatible with that world. If you're building a program in any language and it interacts heavily with a particular library then you'd better write something idiomatic to that library. If you're going to be using several libraries (including your language's standard lib) with different idioms then one of the most important design decisions you can make is how to bridge them, and where to make compromises.
- rmangi 14y agoGreat article. Even if you're not a C/C++ programmer.
- nirvdrum 14y agoThe author seems to like the minimalistic comments. I wonder if the team looked back what their thoughts would be. I can barely look at code I wrote a year ago and not ask what the hell I was thinking, but in my mind it was absolutely clear at the time. I guess an impartial third party reading it and understanding it is a strong testimonial.
- RyanZAG 14y agoThe key is to have self documenting code, not undocumented code. If you create functions that do only a single thing, with their purpose fully described by their method signature then you don't need comments - the method itself explains exactly what it does. The author makes a good point that comments are just more text that you need to maintain, and whenever you make changes you now have to make changes in two places: in the code, and in the comment - (and I guess in the unit tests as well, depending on the change...) The best code I've personally seen has been code with no comments and an attached document explaining how the system works and how modules tie together.
- nirvdrum 14y agoOh, I get the concept. I've just been doing this for a while and see it fall part constantly. The problem is always that what is clear to you isn't clear to everyone else, including yourself somewhere down the road. And decomposing everything to atoms tends to lead to a mess of indirection. Clarity can usually be addressed with longer variable or method names, but there's a strong culture against that in nearly ever programming community.
- scanr 14y ago> Clarity can usually be addressed with longer variable or method names, but there's a strong culture against that in nearly ever programming community. It would be interesting to see a list of those that discourage long method and variable names. I haven't seen anything that suggests that Java, Ruby, Python, Haskell, Kotlin, Scala, PHP, Groovy etc. discourage long variable and method names.
- Snowda 14y agoI currently make a living by essentially applying these rules to other peoples code and handing it off. My second programming lecturer ever, refused to correct my assignments if these rules were not followed. He never told us said rules though. This was before I learned what an array was! Got 0% on my first two assignments with him but eventually he corrected the next ones with a good life lesson. Frankly its the only thing I remember from that asshole but was probably one of the most important lessons in my opinion!
- edu 14y agoComments should be about why a piece of code does what it does not about what (should be clear from the function/method name) or how (should be clear from the code itself). As long as the comment just explains why it should be as long and detailed as necessary.
- martinced 14y agoThat is so wrong. Mathematical thesis are using a syntax arguably much much more powerful than programming languages and yet they're still using lots and lots of english to describe what the formulas are doing and why they're (supposedly) correct in doing so. I very much prefer to have 1000 lines of some Lisp dialect with lots of comments about what the code does than 10 000 lines of "self-explaining" Java/C# code. Code can contain bugs. Comments cannot.
- thinkstoomuch 14y ago>Comments cannot. I disagree. While the comments won't break your code, they can certainly ease the introduction of bugs, and cause cognitive delays when they're wrong or out of date. Comments need as much care as the code itself, with the DRY principle guiding when they need to be pruned.
- HeXetic 14y agoAs someone who has worked with the Doom 3 source code for a mod, I have the opposite opinion. The code very clearly shows a programming team (or programmer) in the process of transitioning from old-school C to C++. Most functions have a huge blob of variable declarations right at the top, as was once necessary in C, even though these variables aren't used until later, or possibly even at all. Usage of const is minimal to non-existent. Global variables are everywhere. It made some of the functions I had to modify so hard to read that I wound up completely editing them, particularly those variable-declaration blocks, even though I ultimately only needed to change a line or two to get my mod to work.
- nitrogen 14y agoWhat do you find so disagreeable about collecting variables at the top of a function? For the most part, I like having all the variable declarations at the top, so it's easy to see what names are in what scope.
- alanctgardner2 14y agoI was reading '21st Century C' (I strongly recommend it for anyone writing C on a regular basis), and the author also argued against declaring all the variables up front. I don't quite know why, but I had a very strong reaction against it. As far as best practices go, I would try to keep every function small enough that you can find all the stack variables easily. If the function gets too hairy, refactor it so you can follow where the variables are going.
- oldcigarette 14y agoI don't see any advantages of declaring variables up front. Not all variables should be at the function scope. Often I have a local variable that is only used inside one branch of a conditional, why should I declare that at the top? Declaring things up front also separates the type from the usage, which makes things harder to read and takes up twice as many lines in some cases.
- ExpiredLink 14y ago> C++ code can quickly get unruly and ugly without diligence on the part of the programmers. To see how bad things can get, check out the STL source code. Microsoft's and GCC's[5] STL implementations are probably the ugliest source code I've ever seen. Even when programmers take extreme care to make their template code as readable as possible it's still a complete mess. Take a look at Andrei Alexandrescu's Loki library, or the boost libraries—these are written by some of the best C++ programmers in the world and great care was taken to make them as beautiful as possible, and they're still ugly and basically unreadable." Finally, a guru dares to call a spade a spade. Header-only, template-only C++ programming is a mistake! After 1995 C++ Standardization took the wrong path and lost contact with real world developers. The glorification and idolization of 'STL programming' was in sharp contrast with programmer's needs. Today in C++ there is a chasm like in no other language between the 'official language' and what programmers need and use day in day out.
- MichaelSalib 14y agoWhat's odd is that the C++ community fetishizes these techniques so much. I mean, when faced with problems that require running code at compile team, the lisp community's answer was "ok, just include an interpreter and run your lisp code at compile time with eval-when". The C++ community first insisted that there was never a reason for doing so and then said "ok, but instead of writing code for compile-time execution using C++, we're going to create an nightmarishly complex and crippled language that you'll use instead; it will be so painful that you'll be proud of implementing factorial". And now a whole new generation of programmers can gaze at the horror of Modern C++ metaprogramming and conclude that metaprogramming is bad....
- pja 14y agoAs far as I know, C++ templates were never intended be turing complete. They turned out that way by accident. If you'd asked the standards body to add a turing complete type-level meta-programming language to C++ in order to generate code at compile time I suspect they'd have told you to get knotted. What people actually asked for was a reasonable syntax for adding generic functions to C++ that would not carry any runtime cost. Sounds completely reasonable, right? The standards group said "sure, how about this?", and kept adding more perfectly reasonable individual requests to the syntax like template specialisation. Only afterwards did the true nature of crawling horror that they'd inadvertently unleashed become apparent.
- greggman 14y agoIt would be really helpful to have some real world examples of changes going from C++ OOP to C++ functional and include the trade-offs. By concrete I mean what changed in Id's code (or some other game or ui framework), and not just some text book example. What changed in GameObject or PhysicsSphereObject or RenderableSkinnedMesh or whatever things changed. What did the code used be like? What was changed? What benefits did the change provide? What problems did the change introduce? Abstract talk is interesting but "show me the Money!" ;-)
- groby_b 14y agoGo search for "data oriented programming". Mike Acton wrote a few interesting presos on it, GameDevADay blog has a few others, too. Yes, technically they have different motivations for going the way they go, but the end results tend to coincide neatly :)
- zxcdw 14y agoI guess Carmack would be a big fan of Rust which essentially borrows lots from Haskell, OCaml, C++ and Erlang while being native, safe and on-par in terms of speed with idiomatic C++.
- pjmlp 14y agoOn the Quakecon where he talks about static code analysis, he mentions that ML languages are too much for games developers, given the usual skill set.
- Jabbles 14y agoI don't have much experience with C++ codebases, but is this really "exceptional beauty"? The majority of the things he comments on could be enforced with a code-formatter.
- zxcdw 14y agoConsidering the codebase is around a decade old, how many code formatters existed for C/C++ back then? I am asking because I've only used gofmt to format Go code and infact it was gofmt which introduced the whole concept to me.
- Jabbles 14y agoAstyle seems to have been around that long, I imagine pure C formatters have existed for much longer. They've got a lot in common with a language parser so if you've got a compiler you're most of the way there. http://astyle.sourceforge.net/notes.html http://astyle.sourceforge.net/notes.html I think Go is pretty unusual in that gofmt is provided with the language. So all Go everywhere looks the same. It's awesome isn't it? :)
- kybernetyk 14y agoI can remember using AStyle in ~2003. I think it's a little older than that.
- roel_v 14y agoIt just shows that the author is an intermediate-level programmer at best, if his main concern about the 'beauty' of code is in how it's formatted (spaces vs tabs! K&R vs Allman braces!) and whether it's const correct. Throw in a sprinkle of complaining about silly comments (because obviously there are armies of people out there defending documenting getter/setter functions...) and the obligatory 'templates are bad because I don't understand them', and you have yet another cookie cutter programmer link bait blog post polluting the general internets.
- eliasmacpherson 14y agoI've always been told that the get, set() idiom is to allow the author to change the implementation at a later date. I always resent doing it but I can see how if the body of the function is not declared in the header file, but instead the associated .cpp file, that an author can change it, without introducing a whole recompile overhead. Writing code that may not be needed is bad, but it's a trade off vs. preventing other users writing code that depends on it when their code should not.
- deleted 14y ago[deleted]
- nostrademons 14y agoA lot of the practices here are enshrined in the Google C++ styleguide: http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml http://google-styleguide.googlecode.com/svn/trunk/cppguide.x... The author's first point about establishing conventions so that you can re-use the code that works with those conventions is very important. At Google, nobody writes code to serialize/deserialize bytes, because the default answer is just "use protobufs". Nobody writes low-level communication protocols (well, outside of some very-specialized infrastructure teams), because there's one RPC system. There's one standardized naming system, and a mostly-standard logs format and method for analyzing logs. If you do batch computation, there're two solutions, and there're only a handful of different file formats and storage engines, certainly less than in the open-source world. I think Rails and Django (and Lisp) discovered the same principle: if you get everybody writing their code & data files the same way, you can write tools to manipulate those files, and that saves you way more in productivity than trying to get the perfect file format.
- zem 14y ago> A lot of the practices here are enshrined in the Google C++ styleguide: with the notable exception of lining up things horizontally, which tends to be frowned on.
- linhat 14y agoI really do not understand why they chose to do so. In my opinion, it does not improve readability at all, even worse, when the spacing between type and name gets big, your eyes need more work to figure out the correct line relations. Furthermore, this can generate horrible commits, for example: int x; int y; becomes: int x; int y; float z; after adding ONE single variable. But your commit will contain changes for the above 2 lines. This will make for an insane experience once your variable list grows to a certain size (including multiple spacing changes) and you intend to rollback/cherry-pick/time-warp. Then you will get to deal with the full wrath of conflict resolution. Also, your commit (of one logical line change) contains way to much meta info, which also decreases commit/diff readability.
- zwieback 14y agoThese are very reasonable guidelines that most of us can probably relate to. What I find much harder is to write "beautiful" code at a higher level. The examples shown are mostly algorithms working with fundamental language features. My code tends to get ugly when integrating APIs from different sources with different conventions. I spend a lot of time checking return codes, mapping from one set of error codes to another. Sometimes it's hard to decide whether a return code has to be checked or whether I should assume, for efficiency, that all parameters I'm sending in or getting out are ok. Other things that uglify my code: exception handling, locks or other concurrency artifacts, retry loops.
- dougk16 14y agoI am also a const nazi, and occasionally find myself trying to imitate some of its uses elegantly through run time errors or strict naming conventions in other languages. I do understand why many other languages decided not to support it though. There's definitely a few times I've coded myself into a corner and ended up with "const spaghetti", having to do a const_cast or two to free myself and make a deadline. The article makes it sound a little like you can just slap const everywhere and your code will be better. It does take more time and effort to be const correct, although it's usually worth it.
- coffeeaddicted 14y agoI also care about const-correctness... but once in a while in a dark hour I wonder if I really saved or lost more time by it so far.
- gavanwoolery 14y ago"I mistrusted templates for many years, and still use them with restraint, but I eventually decided I liked strong typing more than I disliked weird code in headers." I never liked templates myself, and still avoid them almost completely. Even if my reasons are ridiculous, here they are: 1) I typically don't use anything where I don't understand its inner workings (i.e. how it manages memory, how the compiler is likely to optimize it). This does not mean the entity in question is bad, it just means I'm too lazy to learn about it on a deeper level. 2) In most cases where I need to handle a diverse amount of operations and a diverse amount of data types, it is not CPU-critical and I can resort to a higher level scripting language that is much better suited for the purpose. 3) Template syntax does not sit well with me. This is really just an OCD on my end. 4) I often prefer using uber-types (all-encompassing) to many different types -- within reason. I don't like to extend classes for this reason (particularly when you get into extension-hell with 5 different sub types).
- cbsmith 14y agoI'd say 1-3 are perfectly reasonable. If you want to get good, put in the time and you will be rewarded, but that's really a choice. #4... I think that might make a lot of sense with a weak or dynamic type system, but with a static type system there is a lot of benefit to having a different type anytime the behaviour is different. Of course, that can be tedious unless you have templates or at least generics...
- deleted 14y ago[deleted]
- dchichkov 14y agoI've loved John's code since I saw it first time when the original Quake was leaked from their FTP site through IP spoofing. I was just a kid at that time, and it was an amazing experience to hack it. Yet now, the first example that I saw in this article hurts my eyes. Compare: for ( i = 0; i < in->numVerts ; i++ ) { dot = plane.Distance( in->verts[i] ); dists[i] = dot; if ( dot < -LIGHT_CLIP_EPSILON ) { sides[i] = SIDE_BACK; } else if ( dot > LIGHT_CLIP_EPSILON ) { sides[i] = SIDE_FRONT; } else { sides[i] = SIDE_ON; } counts[sides[i]]++; } Versus: for(i = 0; i < in->numVerts; i++) { dot = plane.Distance(in->verts[i]); dists[i] = dot; sides[i] = dot < -LIGHT_CLIP_EPSILON ? SIDE_BACK : dot > LIGHT_CLIP_EPSILON ? SIDE_FRONT : SIDE_ON; counts[sides[i]]++; }
- Cushman 14y agoBravo. That comparison makes worrying about vertical space seem ludicrous.
- jblow 14y agoUgh. I prefer the Carmack version. This example turns the code into something that appears more airy but in fact is much harder to understand due to extensive use of ? :. I find that one of my own major steps toward programming maturity happened when I stopped doing goofy things like this and started writing code that was as simple as possible to logically follow, and that was as un-special-cased as possible. (By this latter I mean, if you change the code a little bit, you don't have to rewrite it; it looks basically the same. Imagine you want to do more than just assign one variable inside the clauses of the 'if'. In the Carmack version you just add more code there. In the proposed substitute, you have to rewrite the whole thing.)
- dchichkov 14y agoThe assignment sides[i] = ... with "? :" formatted as a table (in table notation) should be red as a single statement. It is a relatively common element, you will find in easy to read, once you see it a few hundred times.
- dysoco 14y agoSo he discourages Getters/Setters and instead says that declaring the variable as Public is better? I mean, isn't that like not giving a sh-- about encapsulation principles ?
- sgk284 14y agoWe're all consenting adults. Python gets by just fine on this principle.
- zanny 14y agoThe argument from the Java camp is that if you always use the getters and setters, you leave the access and modification open to extension, say, by adding a callback listener for changes to a variable. I then just say, change it when you need it, and use that fancy "find usages" thing most IDEs and vim have.
- ufo 14y agoPrecisely. The real reason Python gets this right is that it the properties feature lets you write `obj.foo` but still call a `getFoo` method behind the scenes.
- npsimons 14y agoI'm kind of the same opinion as Carmack re: getters/setters. My feeling is, if all you're going to do is allow clients to read and write the variable, why not just expose it? Sure, you can argue encapsulation and even justify it by saying that later down the road you may want to change the implementation, but far too often I've seen C++ classes with a setter and getter for every variable, for no good reason (eg, they're never called, or shouldn't be). I think it behooves programmers to really think about the interfaces their code offers; don't just expose something through setters and getters because it's there, ask yourself, what is this class really offering that you couldn't get with a struct? Pass through setters and getters aren't an abstraction.
- hresult 14y agoI don't like this coding style at all. There are quite a few inconsistency with respect to the formatting, for example: common->Frame(){ session->Frame() { ... } } etc.
- thoughtcriminal 14y agoA bit of a derail, but I have to ask: since Carmack has been big on rocketry for years now, is he in any way associated with Elon Musk's Space X program?