Over a decade ago I used to argue this with the C++ committee people. Back then, they were not concerned about memory safety; they were off trying to do too much with templates. The C++ people didn't get serious about safety until Rust came along and started looking like a threat. Now they're trying to kludge Rust features into C++ by papering over the unsafe stuff with templates, with some success. But the rot underneath always comes through the wallpaper. Too many things still need raw pointers as input.
The encouraging thing today is that we're kind of converging on good practices in language design.
- You really want to declare types for function parameters. The compiler needs that information for checking, and the programmer, and the maintenance programmers who follow behind, need that information so they know how to call the thing. On the other hand, you can usually infer the result types of expressions by a simple forward process. So C++ gained "auto", Rust and Go started off with that arrangement, Javascript got TypeScript, and Python is getting there via a side trip through unchecked type declarations.
- The three big questions in C: "How big is it?", "Who can delete it?", and "What locks it?" C gives no help with any of those. C++ now tries to address the first two. All the garbage-collected languages address the first two. Rust addresses all three.
- The idea that locks should be connected by syntax with the data they are locking took far too long to become accepted. Partly because the Ada rendezvous and the Java synchronized object didn't work well. Even Go doesn't do that very well. But language design seems to be coming around. When and if thread-type concurrency comes to Javascript, the webcrap crowd will create race conditions everywhere without automation in locking.
I think this is a little oversimplified, because I’m pretty sure C++0x had move semantics and usable smart pointers prior to Rust really entering the public conscience/probably before it existed. AFAICT Rust takes a lot of inspiration from how you would “fix” C++, in ways that you could never actually do in C++ because it would break backwards compatibility.
I suppose it depends on what you would consider being “serious” about memory safety.
I view move semantics as not a memory safety thing at all (in fact, use-after-move can be exactly as ugly as use-after-delete), and smart pointers as a “let’s just make things marginally better than raw raw new/delete” thing. Smart pointers in C++ can still be null, after all.
Calling C++ not serious about seems a reasonable take to me. Of course Rust didn’t invent all of its solutions from scratch.
Smart pointers + static analysis + sanitizers is the best options I am aware of for improving the correctness of C++ programs, and I don't think Rust really had much to do with them. The picture painted that Rust made the C++ standards committee scramble to start addressing safety seems wrong; it feels more like there's been a mostly steady increase in safety over time without need for any outside influence.
(a) Smart pointers + static analysis + sanitizers make C++ pretty usable
(b) C++ usability requires brittle, half-baked add-ons such as smart pointers and non-deterministic/heuristic/unreliable/NP-hard development tools such as static analysis + sanitizers.
OK. I think some people mistook me as making a value judgement about C++. This is not true, I am merely saying I have doubts that Rust has had a serious impact on C++ design (yet; if it does it will start showing up soon, though.) If you don't believe me please carefully reread my comments; I really wasn't trying to say anything overall good or bad about C++, just observation about trajectory.
My personal opinions on C++ are not very positive, but it's a language I literally grew with. My experience is that C++ code very gradually became more stable and less buggy. The thing is, some of that is just improved discipline, some of it is static analysis, etc. But a lot of it, is genuinely C++0x/C++11 and features that built on top of this.
So the idea that Rust showed up and suddenly C++ cared about safety? I don't really see it. I think C++ developers started caring about safety and then Rust showed up because of that.
P.S.: While the borrow checker and ownership semantics is really cool, I think a programming language's ecosystem and the 'best practices' it lends itself well to have a greater impact that people completely miss. Rust, Go, Zig are all clearly trying to foster a culture of writing more correct and complete programs. Error handling doesn't just feel like a couple control flow mechanisms you can use, but a responsibility that you have. Modern C++ is getting better probably more because of this than any committee; although I really wish the Expected type would've gone somewhere, since I feel Rust's `Result<...>` type and Go's multiple-return with `error` are being proven right as ways to handle errors robustly even with their caveats. (I've heard Zig's error handling is really cool too, but I haven't had a chance to write a serious Zig program. I'll get to it soon.)
When I think about C++ "smart" pointers, I often think about easy to provoke / difficult to spot fatal edge cases. E.g.
- C++ code where some method calls std::shared_from_this(), and that method ends up being called indirectly during the object's construction, leading to a 0-pointer dereference [1]
- accidentally creating two shared-pointers to the same object leading to an eventual double-free (that may just silently corrupt the heap while the program continues running)
- undetectable cyclic shared_ptr<> references somewhere deeply hidden in the code causing a memory leak
Modern C++ feels like a case of "but we can do that in C++, too" syndrome. Stuff that "just works" in Java or Lisp, can now be done in C++, too, however in C++ it will only work for 95% of all use cases, and break in a spectacular manner for the remaining 5%.
E.g. think about the template meta-programming madness (and efficiency WRT compile-time) and compare that with what Lisp offers (see also [2]).
Both of your first two cases exist when people convert code bases that used raw pointers to shared_ptr. The normal intended usage of shared_ptr is to use make_shared and never have a raw pointer ever exist. The need to us shared_from this (or create a new independent shared ptr) happens only because some part of the code base is still using raw pointers for that particular class.
OTOH, C++ needs the ability to support old code bases because of its age. Of course a new language can forego this.
As for cycles, for GC language using ref-counting under the hood, the same problem applies. I've also seen memory leak from accidental keeping of pointers in fully GC languages. I've seen it happens in Java multiple times. Of course, two wrongs don't make right, but in this case GC is only solving one case.
But the more fundamental issue at play is that pointer cycles usually means a lack of up-front design and thought about how a program will work. As such, it is bad, but only a symptom of a lack of proper initial design which can manifest itself in all kind of places.
I think for shared_from_this() I have a different use-case in mind: an object of some class wants to register with some other class. I.e. think about something like
auto self = shared_from_this();
document.onclick([self](){self->doit(); } );
I'm not sure there is a way to express that in C++ without use of enable_shared_from_this.
It is very easy to accidentally run this kind of code from a constructor where shared_from_this() is a 0-ptr.
make_shared also only helps in 95% of cases. E.g. it does not allow specifying a custom deleter, which is a use-case that is allowed by the shared_ptr<> constructor. It also won't fix any of the problems WRT shared_from_this() being called in the constructor.
> I view move semantics as not a memory safety thing at all (in fact, use-after-move can be exactly as ugly as use-after-delete),
Note that move-destructors were also considered for C++11, and were not added for lack of time / implementation experience . And this is really too bad, because they are at least as important as move constructors if not more so; I've yet to encounter a case where you actually want to use a moved-from object after the move.
Everybody and their uncle have an idea about how they would "fix C++" - but these ideas mostly contradict each other and would make for rather different languages :-)
I’d argue it doesn’t make sense to fix c++. A language is not about what you can do with it, its about what it doesn't let you do. C++ will continue letting you do everything in the name of backward compatibility.
They're called "editions". And it was proposed to the C++ committee, but with things like ADL and SFINAE existing, making any substantial changes just isn't possible. What happens if you SFINAE on a class derived from an STL container with a method removed in Y from a module in edition Y when it is declared in a module with edition X (where it would exist)? Does the method exist or not? When using contracts from different editions which holds? In what context is the question asked?
Even things like integer promotion rules or "trivial" things like "deprecate NULL and 0 for nullptr" are likely untouchable with any edition mechanism for C++ that doesn't end up Breaking the World.
Actually, that point has already come: There is insistence on maintaining ABI compatibility with increasing versions (as opposed to source-level compatibility), which prevents various potential improvements.
Regardless - C++ will effectively has some deprecation, at least of the standard library, with the introduction of modules instead of included headers in C++20.
Everybody and their uncle had a lot of ideas about how they would fix "C++". Eventually all those contradicting ideas got merged into the C++ standard.
This is one of the few times I've seen anybody mention threading in Ada in this sort of discussion. I've become quite fond of the language, and my first brush over the tasking stuff left me fairly impressed/interested. However actually getting into it is something I've been meaning to do, so I don't really have much experience with it in practice.
So I'm quite curious, what problems with it you were referring to?
> The concurrent programming facilities in both Concurrent C and the Ada language are based on the rendezvous concept. Although these facilities are similar, there are substantial differences.
> Facilities in Concurrent C were designed keeping in perspective the concurrent programming facilities in the Ada language and their limitations. Concurrent C facilities have also been modified as a result of experience with its initial implementations.
> The authors compare the concurrent programming facilities in Concurrent C and Ada and show that it is easier to write a variety of concurrent programs in Concurrent C than in Ada.
Rust is not a good example of a language which doesn't require discipline: on the contrary, it requires very much discipline and thinking deeply about lifetimes and resources. The difference to C++ (since that's what you've mentioned) is that the compiler double-checks everything at the end. The advance in programming language usability will not come from arcane tools like Rust, but from tools like Java, Python or Go which make previously complicated things simpler with e.g. garbage collection or coroutines.
Using "scale" is also misleading, because to use again your example C++ is a mainstream programming language and one of the most popular in the world. It towers above Rust by any thinkable metric related to scale - popularity, projects, developers, libraries, etc.
I do agree that discipline isn't enough... in the sense that one can't rely on discipline to achieve safety. This is a well known principle of safety engineering, which applies to all systems, not just SW or SW/HW systems. Discipline remains nevertheless essential, because it's one of the main things influencing the "human factor".
I admit I have a bias here, as I use Rust full-time, but Java, Python and Go are not what I would call languages that require less discipline. For instance, Go's error handling is entirely reliant on discipline! All of the meta-programming and runtime reflection nonsense requires discipline to not abuse and misuse. The worst codebases I have seen in Rust are significantly better than the worst codebases I have seen in languages like Java or Python, and maybe its because Rust programmers are more disciplined on average, or maybe its because the compiler requiring clean code forces less disciplined developers to produce better code.
Perhaps there are two camps here: (1) Reduce discipline by making developers no longer have to consider certain situations (garbage collection fits here) and (2) Reduce discipline by shifting that to the compiler (types fit in here)
> Rust is not a good example of a language which doesn't require discipline: on the contrary, it requires very much discipline and thinking deeply about lifetimes and resources.
My experience is completely different. I programmed a bit in rust recently. Nothing system level. Quite opposite.
I had multiple occasions when after succeeding to implement some part in Rust I thought to myself "wow, if I tried to do that in C++ I would have fallen in so many pit traps by now, that rust helped me avoid by restricting me in some ways".
And I'm very new to rust and I was just coding blindly just by powering through different approaches till one of those compiled.
"I can't use borrow to keep that? What's a borrow anyways? Let's just cram it in Rc whatever it is. I can't return this? Fiiine! I'll create it outside and pass it in."
And in the end it worked perfectly. No weird bugs to find. I felt like a caveman trying to bang rock into shape. And at no point rock cracked, split and or buried me under the rubble.
> ... the compiler double-checks everything at the end
This is exactly what makes it so that the programmer doesn't have to be disciplined. The Rust compiler will correct the lazy Rust programmer, while the C++ compiler will blithely do its best.
I’m not sure “what locks it” is a useful question. Locking is a performance and scalability destroying operation in a time when we care about both. Systems that care about both largely avoid locking (including most “lock-free” locks) altogether outside of rare cases, and in such rare cases the logic is simple. Nothing is lost by avoiding locks with good architecture.
In big multi-core systems, I model the handful of locks by the worst-case access frequency for a core to understand contention. In practice, this is often on the order of 100ms, which means the locks are effectively contention-free and the fast path almost always wins.
"What locks it" can include "owned by different threads". That's a lock at a higher level, really.
"Lock free" data structures are tied very closely to the data being locked, and depend heavily on certain CPU operations being atomic. This is more of an issue with ARM than with x86, because ARM needs more fence instructions. Really, "lock free" programming is just locking critical sections with lower level hardware primitives.
The opposite extreme is POSIX locks. Locks are the OS's problem, and nothing in the code says what's covered by the lock. That was once standard practice and is now recognized as being bad.
> Really, "lock free" programming is just locking critical sections with lower level hardware primitives.
I have to disagree, as the more interesting lock free algorithms use atomic operations that can fail. Yes, a compare-and-swap is like having a critical section on modifying that particular address. But a compare-and-swap can fail if what is currently there is unexpected. The result of that failure generally means redoing a bunch of work, rather than just trying again. Interesting lock free algorithms tend to have the structure:
1. Read some data.
2. Perform computations on that data.
3. Trying to commit the result of that computation. If someone else committed data to the same location after you read it in step 1, go back to step 1.
That's less like a "critical section," and more like a transaction. For lock free algorithms that just rely on atomic operations that cannot fail (such as an atomic increment), then yes, those are just like having a lower-level critical section. (And, on some architectures, that's exactly what they are.)
What's important is that you no longer have a critical section, where mutual exclusion is enforced. The only piece of code that has mutual exclusion is an attempt to commit the transaction. The kind of thinking required to implement such algorithms is quite different than when you can rely on mutual exclusion.
(Also, not all architecture use hardware mechanisms to enforce actual mutual exclusion. For example, Power does not, as it uses load-linked, stored-conditional: https://en.wikipedia.org/wiki/Load-link/store-conditional. On Power, even things like atomic increments are implemented with LL/SC. Architectures basically have to decide: will they implement atomics through some kind of a lock mechanism, or some kind of LL/SC mechanism.)
You still have a critical section; the cacheline is in an exclusive mode during the change. LL/SC operates on top the cache sharing protocols, just like everything else. Your wikipedia link even refers to that: "Typically, CPUs track the load-linked address at a cache-line or other granularity [...]". MESI and friends are that mechanism.
What lock-free means in this case is that an OS level thread freezing doesn't prevent global progress. That's true on the abstraction level of typical programming, but grandparent is very much correct:
> Really, "lock free" programming is just locking critical sections with lower level hardware primitives.
Compare and swap is interesting. It's a tiny transaction. It can even operate across networks - FireWire had hardware compare and swap support. Compare and swap with retry may have to wait and retry, which is effectively waiting for someone else to stop using a resource you want. So it's a form of a critical section lock.
> Really, "lock free" programming is just locking critical sections with lower level hardware primitives
Can fence instructions really be seen as locking mechanism? While a lock generally is bound to some data, memory fences are bound to relationships between data or a piece of code. I don't know if Rust or any other language can help you with that.
Isn't the actual bad practice to expose locks at all? If the locking is done implicitly in API calls, one doesn't need to ask "What locks it".
The fences aren't a locking mechanism, at least not on their own. The lower (hardware) level locks are the steps in the cache coherency protocols (potentially including explicit cache line locking signals) that allow an execution unit to, eg, perform a correct atomic compare-exchange operation on a 32/64/128-bit piece of data.
See also, eg, the LOCK prefix on x86 instructions.
(disclaimer: I only have approximate knowledge of anything, I'm not an expert)
Agreed. But! Not all architectures implement atomic operations by locking the bus, as x86 does. The Power architecture uses load-linked, stored-conditional (https://en.wikipedia.org/wiki/Load-link/store-conditional) where when you load a value, you can say "pay attention to who modifies this value," and when you go to store the value, you can say "only actually store this value if no one else modified it."
> Locking is a performance [...] destroying operation
That is incorrect. Properly designed and implemented mutexes (like in the parking_lot crate [1]) are extremely fast in the uncontented case, and need just one byte of overhead (and no heap-allocated memory) per mutex. In the contented case, a lock-free algorithm still has to deal with arbitration between the different threads, so it's typically not faster than a lock-based approach. Lock-free algorithms do have certain advantages (like deadlock-freedom), but performance is typically not one of them.
> Locking is a [...] scalability destroying operation
Also incorrect. If you have N threads trying to append to a single queue, then you have a scalability problem whether you're using locked or lock-free data structures. The solution in such a case is fine-grained concurrency and a different approach, not lock-free algorithms.
> it's typically not faster than a lock-based approach
A common train of thought seems to be "locks are slow, so lock free must be fast", while lock-free says nothing about performance -- only about forward progress.
The question is "What prevents me from making changes to this variable without breaking everything".
So the point is not "What locks guard this data" the point is, how do I determine whether I can currently modify (or even read) this data. This allows the compiler to check that, indeed, you never use the data in a way that would break things. Hence your compiler can then give guarantees about thread-safety.
Now it seems likely that, when compilers do this and you write wrong code, your conclusion might be "I need a mutex lock for this", or if performance matters, "I need to rewrite this to allow performant lockless code. But the main question remains "Can I currently modify this data without issues".
The compiler needs that information for checking, and the programmer, and the maintenance programmers who follow behind, need that information so they know how to call the thing. On the other hand, you can usually infer the result types of expressions by a simple forward process.
Possibly unpopular opinion: I am wary of relying on type inference for return types, other than for a function defined locally where you can easily see the implementation at the same time as the code calling it. Code like
auto x = calculate_some_complicated_thing()
...
do_something_with(x)
has a similar obscurity problem to code like
do_something_impressive(true, false, false, true)
in that to understand what the code is doing at this level, you have to read the interface documentation for the function you’re calling anyway, and then you’re hoping that you get everything in the right place because your tools might not be able to warn you if you don’t.
In practice, type inference for return types works fine for the work I do (iOS development). Initializers are blindingly obvious anyway, and other functions are often members of an object or a struct. There are barely any bare functions.
Making the locks public, I sort of get (like making any field public) -- you have multiple locks, you need to ensure they are accessed in a certain order and if you make them public, you cannot enforce that order.
"The encouraging thing today is that we're kind of converging on good practices in language design." - doubtful that we'll arrive at good practices in language design through incremental steps from a flawed base. All the mentioned languages are low-level.
"Therefore... what? Just give up? Or start over from a non-flawed base?" - those would both be ways to avoid the presumed local optima, yes.
"Which is what? Still to be created, or does it exist?" - several different 'initial stakes'/bases in the space of programming languages exist/were made/can be created than the family of languages mentioned in OP. Exploration from those might yield better optima. The widespread adoption of the current industry 'standard' languages seems an accident of history, not guided by quality of language.
"Are the bases flawed because they are low level?" - yes. A significant part of the computing field is about abstractions. Improving the level of the technology is one of the arguments the article here makes too. Besides, it's the implicit admission of everyone who doesn't advocate/write assembly.
I really think lock-free concurrency models are the long term best bet. Locks and shared memory scale pretty badly, on top of being super hard to get right.
Lock free algorithms and data structures are even harder to get right! And most of the canonical descriptions of them assume that you have a garbage collector and that memory allocation is a bounded operation. Neither of which is really true when you're working in constrained situations.
On top of that, locking is faster than most lock-free strategies unless you deal with high contention or deterimistic guarantees (90% of the latter is solved with bounded fifos).
When I say lock free, im suggesting concurrency that doesn't assume shared memory. I think the notion of multiple cores working on nearby memory blocks just is a bad way to do this, and so threaded models are not great. More along the lines of an actor model or how gpus subdivide work.
That's a bad example: GPUs are all about shared memory, concurrently accessed by many threads. It's up to the programmer to make sure that there are no read/write or write/write data races.
We're probably talking different GPU coding techniques... I'm not talking about things like annotating C++ code to run on a GPU, I'm thinking shaders where it's very hard/impossible to have side effects outside of what that shader is run on.
So a task/job system with explicit dataflow, where data is handed off to the task that uses it, rather than being shared? A lot of game engines do this nowadays and it works quite well. It doesn't eliminate synchronisation completely, but it does simplify things a good bit.
Ideally, any shared data is immutable (and modifying it would then need to be done in a coordinated synchronised fashion) while most mutable state is only ever owned by one task/thread, with ownership explicitly passed between them as others need access, unique_ptr style. Reality doesn't always match with the ideal though.
most mutable state is only ever owned by one task/thread
For many applications, this is the way I prefer to structure code these days. If you know that the only part of your program that will touch certain state directly is always running in a single thread, a lot of problems just go away, because whatever you do in response to any given message or other signal is effectively an atomic transaction, regardless of any concurrency or asynchronous behaviour you have going on elsewhere in your system. You still need to figure out how your message passing or other signalling infrastructure is going to work and in particular what will block and what won’t, so it doesn’t answer all of the usual questions with concurrency, but it’s a widely useful strategy and it goes a long way toward keeping things manageable.
IMHO, types for function parameters depends on the use case.
If you have a function in a standard library, types are obviously helpful, albeit it can be convoluted if they are templated.
On the other end of the scale, if you have a little helper function used to reduce duplication/increase readability in another function, explicit typing easily becomes busywork. This is where duck typing/templates shines.
I once sent a suggestion to the C++ committee people that they allowed easy templating of function parameters (something like "auto foo(auto bar)"), but I don't think people who are not used to duck typing appreciate how much it helps in the grunt work inside modules. At least, I didn't myself.
Well, you'll be happy to know that C++ pretty much allows that, but not for templates. You can use auto in parameters if the calling context allows inference, for example when writing lambda.
Similarly, you can use auto as a return type if the function implementation is immediately given and the compiler with infer the return type. The new starship operator (<=>) typical usage is pretty much declaring it with an auto return type and "= default" implementation.
I quite like the Haskell culture for this one: you specify the type signatures explicitly for your top-level functions, but you don’t usually specify them for nested functions. Put another way, anything with an interface you might need to understand somewhere else should be explicit, but anything only used very locally doesn’t need to be.
There's something I've always been curious about wrt `auto`, and that's why have the keyword at all? In Swift for instance, one can just use `var` or `let` to express mutability and have the compiler infer the type. Is there a particular reason C++ can't, or perhaps chooses not to, do this?
I'm not sure there's a particular difference between the two except the keyword chosen. You type "auto my_var_name = func_call()" or you type "let my_var_name = func_call()". In both cases you get an inferred type for a variable.
Mutability is very different in C++ though - const is quite a complex topic, and isn't as simple as "this variable binding is/isn't mutable".
Personally, I think Discipline is one of the things that separate “coders” from “engineers.”
This sounds like it’s really a treatise on “learning to build a house by making your own nails.”
Of course that won’t scale. Especially if most houses, these days, are prefab, and don’t use too many nails.
But learning how to stay out of flood plains, and selecting good prefab sources, is vital to being a builder. The first one is a fundamental discipline, that also applied to “make your own nails” building, while the second is unique to the new way we do things.
I like to write code that has a future. I want people to be able to take it over, so I use header documentation, and a consistent coding style.
I originally learned Machine Code (I started as an EE). I’ve written embedded operating systems and drivers.
A lot of what I learned, doesn’t apply to writing high-level application code in Swift (what I do nowadays), but it did give me the discipline to power through the “boring” stuff that is vital to writing good, maintainable, extensible code.
I think maybe there is a confusion between “discipline” (the type that manifests as patience, consistency, and craftsmanship), and “experienced inflexibility,” which is not always applicable (and sometimes, downright destructive, like old-fashioned C++ programmers refusing to use smart pointers).
Our contemporary corporate and social culture doesn't encourage or reward discipline. We produce mostly throwaway stuff that has to be released as soon as possible on the market, or else the company goes out of business. The incentives for craftsmanship and diligence are just not there.
> Our contemporary corporate and social culture doesn't encourage or reward discipline
I think there is some selection bias here. The majority of particiapants in HN discussion generally people who work for startups and people who work with web technologies. The Silicon Valley culture might be focused around time to market, but there is a large (mostly unheard) group of people who exist in a very different culture. For example: C is still an incredibly popular language (for better or worse), but on HN you won't hear much about it outside the Linux Kernel.
A lot of this enterprise software development suffers from some very different problems (the time to market thing brings benefits in terms of making productivity important) but also has benefits in terms of thinking about investments long term (when done well).
I do get it though; there is a lot of things you can do with code in addition to the minimally viable, but the cost is time and effort (short-term and long-term (e.g. maintenance)), and eventually money will run out.
You have to set a boundary for "good enough" somewhere, else you end up stuck in analysis paralysis or whatever a catchy term is.
That said, today's development tooling helps you have a baseline of quality pretty quickly; languages that do not have memory issues, compilers and typed languages, etc.
Sadly, you are correct, but that’s not new. This has been the case throughout history.
I was just thinking about the difference between “good,” and “excellent.”
If we look at the difference between a Henkels kitchen knife, and a Japanese Takayuki knife, we are talking exponentially divergent costs. The Takayuki is definitely a lot better than the Henkels, but is it twenty times as good, as the price difference might suggest?
But Henkels makes far better knives than most of the disposable crap out there. It costs more; usually two or three times as much as a “standard quality” knife. Worth it, though.
That’s sort of the “sweet spot” I aim for, and we don’t get there, without discipline, consistency, and patience.
Germany seems to be doing quite well in producing exactly those products from the upper-middle tier – mixing a reasonable blend of longevity, ingenuity and ergonomics, at an earthly cost. In the rest of the world and especially in "emerging markets" however, such products are often unaffordable, uncompetitive and even sometimes seen as old-fashioned. A household in India that goes from not having a kitchen cutlery set to having one cannot ever distinguish its level of quality because there is nothing else they can compare it to.
Maybe you think craftsmanship is better in the past because of survivorship bias.
For instance, the 60s and 70s had a lot of great music come out of it, but have you ever gone down a chart of the hits from decades ago and listened to the lesser known artists? Lots of it is bad and utterly forgettable.
Same goes with a lot of other fields. We only remember the best and forget about the rest.
Exactly. With so many people out there advertising themselves as "software engineers" but being "coders", companies fail to produce maintainable code without a bunch of strict rules. Coding manifestos are a must these days, otherwise you end up with a codebase that looks like the Tower of Babel.
I don't think your worldview is inconsistent with the authors.
Software is one of the few areas where we can easily and rapidly change our tools and the medium we work with. To be professional in this field you must use that ability to systematically remove issues where possible.
To fall back on the mindset that being a good engineer means eschewing those safeguards and relying on discipline is more about image that outcome.
Completely agree. Discipline is only one of many tools at my disposal.
One of the most important things about being a software engineer, in my mind, is the ability to be flexible (I won't say "agile," as that word seems to be pretty much worthless, these days).
One of the disciplines that I've developed, over the years, is writing flexible code; Code that has a future.
One of my favorite text editors is BBEdit[0]. I've been using it since the 1990s. During that time, I've tried others that have been raved about, like Sublime, TextMate and VSCode. I've liked them, but always ended up going back to BBEdit, which has been doing a great job of adapting to the new operating systems and workstyles.
The fact that the app is nearly thirty years old, and started off with a bus factor of 1, says that the original author applied discipline and a "future-facing" design ethos, back in 1992. I can respect that.
I also use Adobe Photoshop, which I first met before Adobe brought it, in the late 1980s. That seems to have grown well.
I hate these articles that feature a programmer who works within a certain domain and assume all programming tasks are similar.
Yes, it would appear there is an anti-pattern of programmers that feel like going lower level for certain things matters. It doesn’t always. Programming computers has multiple levels of abstraction for solving different problems.
But there’s also this idea that the magical compiler does good enough abd there’s no reason to challenge the assembler. But there are programming problems where that is in fact essential.
It’s not a kissing contest. Writing great asm or C is as challenging as writing great css. But to be sure, knowing what your code actually requires hardware to do is really valuable. It matters and we don’t have abstraction in many cases to abstract that away totally.
It doesn’t hurt to have a good idea on how the machine works.
The point of the article isn't to belittle someone for having a good, deep knowledge of machines. The foundation of most code written is built by those people.
The point of the article is to point out that languages, frameworks, or apis that expect a level of discipline to write well are inferior to ones that don't expect discipline out of its coders.
The takeaway shouldn't be - I as a programmer shouldn't learn how the machine works.
The takeaway should be - if I'm designing an API/Language/framework, I should figure out how much I'm relying on an engineer's discipline to make the code great (performant, robust, secure, etc). If I can reduce that reliance, the API/Language/Framework becomes better.
I like the focus on scale in the title, and think it’s very apt.
Requiring discipline is not superior or inferior, but doesn’t scale. It’s then your to decide how much it matters: do you have/need to only focus on a small group or are you tools targeted at a wide audience or you want it to widely succeed ?
The answers to that question won’t be the same for everyone.
My usage of inferior/superior is contextualized mainly to scaling but the article mentioned many more things other than scale in the anecdotes (like producing better code overall). Maybe the vagueness of the word choice is a bit controversial in this definition-sensitive environment.
Well, and here I thought that discipline doesn't scale because it is not a universal concept. Having "discipline" in C will not transfer to better discipline in other languages because each language has a different feature to abuse. C might have malloc/free with catastrophic consequences but C++ has templates which lend themselves to a different type of abuse i.e. excess complexity.
> “ The point of the article is to point out that languages, frameworks, or apis that expect a level of discipline to write well are inferior to ones that don't expect discipline out of its coders.”
I think this is what the parent comment that you responded to is saying is totally bogus.
A good example is Python. In my company, the teams using Python run circles around the teams using C++ and Java. The Python teams ship higher quality stuff that is both more reliable (it turns out that lack of a compiler for various static checks not only doesn’t make anything more reliable, but also slows things down unreasonably on large code bases), and easier to modify and adapt to changing requirements.
> Writing great asm or C is as challenging as writing great css
I would argue that writing great asm that is more efficient than your idiomatic C code is going to be a very very challenging task on modern non-embedded CPUs. The compiler definitely knows more than I do about how the internal architecture works and which instructions to reorder to prevent stalls etc
Writing assembly pales in comparison to tracking down some of the more ridiculous CSS problems.
Winforms got a lot of hate for some reason I don't understand, but designing with it in Visual Studio is the easiest, best, most sane layout/designing experience I've ever had. It makes me wish the web was more like that.
This easily turns into a strawman. Does writing good C code include checking the disassembly and reworking the C code when the compiler produces bad results?
I've seen several examples of the compiler being brilliant until suddenly a simple change makes it not brilliant at all anymore. Compilers are just tools.
> Yes, it would appear there is an anti-pattern of programmers that feel like going lower level for certain things matters. It doesn’t always. Programming computers has multiple levels of abstraction for solving different problems.
The thing is, sometimes you need to know how those lower layers work in order to make that judgement. If you’re writing networking code in JavaScript and don’t know what sockets are, you’re going to have a bad time sooner or later.
I hate these articles that feature a programmer who works within a certain domain and assume all programming tasks are similar.
Citation needed? Last I checked, Graham was working on a C64 Smalltalk80 VM as a side project, so I presume he's not completely ignorant of low-level development. And statements like "Your computer is not a fast PDP11" are aimed directly at low level development.
> But there’s also this idea that the magical compiler does good enough abd there’s no reason to challenge the assembler. But there are programming problems where that is in fact essential.
Citation needed. I've seen people claiming this for years, and I've yet to see a single case where handwritten assembler actually did better than spending the same amount of effort on speeding up the compiled program (e.g. taking 5 minutes to actually set the right target architecture).
> Writing great asm or C is as challenging as writing great css. But to be sure, knowing what your code actually requires hardware to do is really valuable. It matters and we don’t have abstraction in many cases to abstract that away totally.
> It doesn’t hurt to have a good idea on how the machine works.
On modern hardware both C and ASM are distractions. If you actually want to understand the performance you need to look at cache misses and mispredicted branches; reading through C or an ASM listing will only serve to mislead you. Strangely enough, the crowd that wants you to learn C seem to be the same people who are vehemently against using the kind of profiling tools that will actually tell you what's going on.
I spent time with a team that wrote math functions in assembly. They also wrote them in C so they would be more portable. Their aims were similar for both - similar accuracy and performance.
The C versions were usually slower - sometimes a lot slower. They knew all the optimizations the compiler was doing and tweaked as much as they could.
In general, though, you're right. I'm particularly annoyed at universities that insist on making their students do all their microcontroller labs in assembly. When I took the course, the professor made us do only two assignments in assembly, and the rest in C. He said it had been over a decade since he had encountered a real world microcontroller problem where one needed the assembly - and he said this some decades ago.
I don't do microcontroller stuff for a living, but anecdotally, the only people I've met who insist one must program in assembly for microcontroller problems do not do it for a living.
The ARM Cortex-M series was designed from the ground up with the goal of being 100% programmable in C. And it has succeeded at that goal, with only a few rare exceptions.
The times I have found it necessary to write assembly (as opposed to just reading compiler output) in the past few years of microcontroller programming seem to fall into three main categories:
1. The vendor-provided startup code is broken and it needs to get fixed. Usually this is indeed 100% written in C for Cortex-M cores, but some vendors get... creative. Vendor code is special. Vendor code has always been special.
2. The compiler is too stupid to use a particular processor feature. This is usually related to performance, and in particular to the math and DSP instructions being too complicated for the poor compiler to understand. This is relatively rare, but when it's in a hot area, a single intrinsic call (sometimes you have to write the intrinsic...) can make a huge difference.
3. You need to debug something hairy and low-level hacks can help you do that. Usually this kind of debug solution only occurs to people who already have good low-level intuition; everyone else just finds a different way to solve the problem, which ends up working out just fine either way.
> Citation needed. I've seen people claiming this for years, and I've yet to see a single case where handwritten assembler actually did better than spending the same amount of effort on speeding up the compiled program (e.g. taking 5 minutes to actually set the right target architecture).
Take some time looking at established open source projects where IBM has heavily invested in optimizing for the POWER architecture. I'm thinking things like glibc, gmp, Golang, openssl, ... The results they get from their hand rolled assembly far exceeds what gcc/llvm spit out. (At least, it did 2-3 years ago.)
Back when the Linux Technology Center (LTC) was a thing, I was fortunate enough to meet many of the individuals working on these projects. They are all wizards. Brilliant.
One of them retired recently and went on to start pveclib to make some of these optimizations in number crunching more accessible to others: https://github.com/open-power-sdk/pveclib
I was once in his office asking about assembly instructions for SHA-3 (a project that sadly didn't go very far) and he could quote useful scalar/vector instructions to me and their timing/latency stats faster than I could find them in the manual.
> I've yet to see a single case where handwritten assembler actually did better than spending the same amount of effort on speeding up the compiled program (e.g. taking 5 minutes to actually set the right target architecture).
C++ with intrinsics can be very close to assembly. Compilers allocate register and do their magic with the rest of the code, but the real workload translates into assembly almost line-to-instruction. Takes longer than 5 minutes to do, these instruction sets are less than straightforward, but I think 80% of real-world performance sensitive compute-bound code is written like that (the other 20% being assembly).
> that will actually tell you what's going on
When I work on performance critical pieces, I already know what's going on. I design my code from the start to minimize cache misses and branches. The profiling tool I find most useful is sampling profiler. Then I either read assembly and try to do better, or rework higher-level logic or data structures.
Branch mispredictions and cache misses might be hard to see in C and assembly, but in interpreted or JITted languages it's borderline impossible to reason about those things. Even compiled languages have trouble there if you cant control memory layout.
As a Mid Range Millenial, I fall right into age range who cut their teeth in an era just past this guy: the jquery web dev. It's amazing to read this because I think I was part of the group that began the exponential curve of new technologies. I wasn't exposed to this "hazing" at all. I was believed in. I was valued. I was encouraged to explore.
The religion of the time was to work on things that increased developer productivity time. Performance be damned. It flowed naturally at the time. Computers were (and are) fast enough to create these web apps! We just need to get more features (and better looking ones!) out the door!
And now here I am sitting on the other side, looking at all my peers work and thinking "slow down, everyone. We've made too much stuff. We need to organize and catalogue and refine and optimize"
It's like culture is a Tick/Tock cycle. This guy was a Tock - a refinement of existing technology. I was a Tick - an explosion of new technology.
And now (I believe) we are witnessing another Tock - a refinement, a culling of the weak, a centralization.
I love reading the historical programmers account. As I near 30 I realize, that I will become that, a history lesson for the fresh minds of the era.
I think I have lots to teach you. I need only to refine, cull, centralize, and catalogue my thoughts. Piece them into theories of abstraction, class hierarchies of ideas, if you will.
For I experienced the polar opposite of this. I experienced the explosion.
And yet still, I agree.
Discipline doesn't scale. This encoding of discipline into system. I agree with it.
That defines a large part of my job - to take the "best practise" and encode it into the day-to-day.
This concept: "discipline doesn't scale" is fundamental to our history. It seems trivially true.
This prose is excellent, this history lesson is important and valued, and the fable of this story rings true.
Short of external factors (sharp recession in tech), I don't see the culling phase happening.
If a recession did happen and orgs started cost cutting, I imagine that R&D would drop somewhat, and you would see fewer new projects and toolkits perhaps? Even then its hard to see the pace of innovation (for lack of a better word) slowing.
It seems to me that technology is now in the stage, where its everywhere. Every industry that is quick to adopt new software gains a competitive advantage over its peers, in so many industries. This unreasonable effectiveness of software seems to feed itself.
I would love to see studies that measure/quantify the blooming and culling of technologies so we could talk about these things with more than just anecdata.
Discipline for its own sake is certainly wasteful.
What grieves me is lack of inquisitive ness and communitcation. If we're not deepening our understanding of how things work, and working together, then things get wobbly.
True story: delimited text data ingest file. Guy wrote a C language file to hack the upper byte off the characters, and then write them back to disk. At a glance we think "C! This was a performance thing." Except laboriously writing each character after hacking it in twain and discarding the upper byte isn't a performance hack. It's a "didn't RTFM on the python open() command and try the encoding argument, so we did a C hack" move. Encoding="utf-16" worked great.
I'd take a victory lap, except for every time I'm clever, there are two where I look an ass.
Discipline may not scale, but teamwork and inquisitiveness paper over a multitude of shortcomings.
I think it's also important to remember that these things we sometimes consider character traits can be quite situational:
In the absence of deadlines or other external pressure, I sometimes write beautiful, uselessly incomplete code. With mild deadline pressure, I do all right. In a toxic, death march environment, I can see how teamwork and inquisitiveness might drain away and someone would write the terrible code you just described.
(See also: fundamental attribution error. I wrote the bad code because of the death march. He wrote the bad code because he's intellectually lazy.)
I've seen quite a few junior developers that either lack the "inquisitiveness" or that it's too weak to overcome even a self-imposed deadline. They get things "working" but don't take time to understand the system first.
As senior developer, I have to go in and stop them repeatedly and make them rework it. Up until now, I've told them why it's wrong and that basically does all their thinking for them. Now I'm wondering if I need to drag that step out and make them come up with the solution instead.
The problem I've been debating lately is how to instill that inquisitiveness in my junior and mid-level devs when they lack it. (I'd been saying they don't think holistically, but I think inquisitiveness is a good work for a trait that will lead to that.)
I'm thinking maybe I change my code reviews to have some video chat time where I lead them through the problem instead of telling them.
While hardware engineers come up with faster and faster computers, software engineers come up with slower and slower and slower software.
While I agree that having nice and easier thing is nice, and easier, performance is always relevant, if not for pride or principle, at least think about the environment, computer processors and memory chips consume electricity, and are really inefficient.. How many percentage of the consumed energy turns into the desired product (computation)? Les than 1%, a CPU using 100 watt is pretty much a 100 watt room heater with a side-effect of also performing some computation..
That's a _WAY worse fuel efficient than a Model T, and yet we treat computational power and memory capacity as infinitely cheap.
The key is that hardware has marketpreassure to become faster and faster. Software gets faster if there is market preassure (see gaming & financial). When the feature backlog is more important than performance that is obviously what is prioritized. You obviously can't work on everything.
The feature backlog is rarely more important than performance but you often see it being pushed ahead of nonfunctional concerns for presentational reasons.
For the most part users or companies buy features, not performance. You can have the fastest software ever but if it doesn’t have the features I actually need to use it then, sorry, no dice.
If performance is one of your features then fair enough, but it’s not for a lot of software.
This is NOT true. There is almost always an expectation that your application have a reasonable performance profile. It's often not a captured requirement, but I guarantee you once things slow to a crawl you WILL get an escalation.
Do you know what happens when escalations become too frequent? Progress grinds to a halt as developers have to churn out of what they're doing to address non-functional issues.
Somebody operating at a product owner level rarely has any concept of this stuff. It's up to the engineering team to be the bad guy and tell him or her that they're not going to get their laundry list satisfied in the time that's available and actually deliver usable software.
The problem with your statement is that it was unbounded. What you are saying is “when your performance is bad, features should take a back seat”. That i agree with.
But “reasonable performance” is relative and context dependent
No, I was presenting my experience that when given a choice of features over “quality” (however you choose to quantify it) we almost always see a push for the former. Unless of course you have a robust communications arrangement with your techies.
That's a quite unobvious consequence of the software progress. Making software be "a bunch of bricks for creating applications" do come with costs. And the main cost is performance.
Where do you get 1% from? We're several orders of magnitude further than that from the Landauer limit, if that's what you're referring to.
However, there is considerable economic incentive to keep Koomey's law going - mostly from mobile these days, since increased computations per joule means longer battery life.
Do you think the market is not pricing it correctly?
In the dawn of computing, machines were far less common, much more expensive, and society was willing to pay programmers boatloads of money to write optimized code because that was cheaper.
The hardware engineers did an excellent job, they reduced costs of computing by a lot. Society is not willing to pay programmers boatloads of money to write optimized code; because hardware is cheap, they can do fine without that optimization.
Whether we thing the pricing is correct or not is a different question I guess. We could theoretically raise computing prices artificially (say through taxation).
Performance-wise, it's bottom of the barrel quality. A single developer has done, in this compartment (Ripcord), much better than their team did. If we want to consider similar languages, Microsoft did enormously better with VSC.
The result is, sadly, that Slack is still widely used.
Whenever I hear the term discipline in the context of computing I always think of Dijkstra's excellent book A Discipline of Programming[1]. His definition of discipline makes a lot more sense to me than the one in this article. And discipline in the sense of proficiency with the predicate calculus absolutely does scale.
One of the most important reasons why we write programs, is because computers are slow and have limited memory. Computers only appear fast due to a host of tricks. The most important trick is the memory pyramid: with at the top a small number of very fast accessible memory (think CPU registers) and at the bottom a very fast but many orders of slower accessible memory (think the whole of the internet). And a lot of business is about moving data up and down the pyramid.
It looks like most software engineers are still unaware of this. Why do we still encode Unicode strings as UTF-8 strings in memory, why not as double linked of Code points? Not because we cannot, but because it is faster and uses less memory.
Another big problem is related to multi-user modifications of shared data structures and that high-level semantic modelling is hard for most people. If multiple agents (people) are modifying a data structure, then those changes need to be combined in a semantic correct way. A complication factor is the fact that most data structures are distributed (also due to the memory pyramid) and that a lot of effort is needed to synchronize modifications.
The double-linked list was just an example. Could also have been any other list container for Unicode code points, where each code point uses a fixed size of memory (at least 21 bits). You might even use a data structures where a 'base' character with (optional) combining characters are treated as a single unit, because for many operations they should be treated as a unit.
>but I do occasionally still hear people telling less-experienced developers that they should learn C to learn more about how their computer works. Your computer is not a fast PDP-11, all you will learn is how the C virtual machine works.
Yes, this is wrong in that sense, but by learning C you learn how C works, and therefore how your computing systems, which are going to be C at some level because we live in Unix's world, work.
I think there is a meme (in the true sense of the word, not a silly picture) where the basis of languages like C and assembler is implanted in say JavaScript and how it does things. For example references vs. values and how this affects closures. Why have a lexical scopes? All this stuff which confuses the fuck out of new programmers, might be simpler if they learned C first.
I think this can trip newbies up. Especially from bootcamps, who don't understand why their code is working, and I feel like replying with "well think about how the object is stored in memory" because none of the stuff that's built on that will make sense without it. I'd like to understand more myself. It would be useful to have some idea of what the compiler is doing with things like lexical scopes. I think that's the only way to really understand them as the abstractions are too leaky.
I think that’s all still addressed by him saying (paraphrased here) “learning C doesn’t teach you your system; it teaches you the C virtual machine.” You could just as easily teach the usefulness of lexical scoping and memory layout by teaching people a simplified version of a modern JavaScript engine and teaching them to inspect the accompanying intermediate representation. Bootcamps don’t dive into these things and give attendees intuitions for some model of computing because they know what people are paying for (learning the level of JS necessary to land a job as quickly as possible).
Although I think learning C opens up your accessible domains of programming widely. I mean, if you know C it doesnt even take a weekend to learn (the basics of) CUDA. Or you can just buy an simple embedded system and do data aquisition easily. Also there is a reason C is often under the hood of other programming languages like Python, Tcl, and (I read on the internet) Julia, Ruby and Perl.
I dont mean you can then write these languages, but you can look under the hood and read the source code (also if you want to write extensions it is useful).
> if you know C it doesnt even take a weekend to learn (the basics of) CUDA.
You mean, of CUDA kernel programming. The basics of CUDA as an ecosystem are not that much about a particular language and there are bindings for many languages.
> The performant is often not talked about in the same sentences as its usual companion species, the irrelevant.
What I find is that programmers I've worked with who have been the most preoccupied with performance always seems to have focused their optimizations in the least valuable areas. It's as if performance mania is directly correlated with a kind of architecture blindness.
I think that's because senior developers already do most of the easy/useful performance stuff the first time around. They design things so that they'll (probably) be performant the first time around because they've had problems in the past in that area.
Mid-level developers haven't had a lot of those problems yet, but they have read articles about them, and those articles usually do silly things to try to prove a point in a simplistic situation.
We had a developer that was obsessed with getting rid of jQuery for performance reasons, but repeatedly failed to recognize that the raw JS functions he wanted to use didn't work on IE and/or Safari, IIRC. Beyond that, we also supported Android 4.4's webview, and he didn't even have a way to test that. He ended up leaving the company for other reasons, but it was a constant battle to keep him in check with that stuff.
(It required compiling the Cordova app, and so long as you kept doing things the same way as the rest of the codebase, it almost always just worked. So we didn't require junior/mid-level devs to test it on every change. It did get tested eventually before the app got a new version, though.)
This week I had to deal with the exact thing you mention. A pull request for "performance optimization", basically a proposal to unwrap a higher abstraction to do it at a lower level, manually and directly.
Yes, it does save a few function calls and instantiating a class - but that's not where the real bottleneck is. The proposed changes would require rewriting large amounts of code elsewhere, making the logic more verbose and complex.
It took some effort to convince the person that the trade-off isn't worth it. I tried to be diplomatic, but I'm afraid they took it personally that the pull request was declined.
I wish I could have proved my objection with some metrics - how many milliseconds were shaved off by the proposal, versus the increased "cyclomatic complexity", as a measurement of its detrimental effect on the code base.
That burden of proof sounds backward; the person making the optimization should be benchmarking.
Rather than rejecting outright, did you consider blocking the pull request and asking for benchmarks demonstrating that the optimization was worth the complexity increase? Maybe they would learn more from that process than from an explanation of why they are misguided.
Good point - I could have asked for a benchmark to prove that the optimizations are worth it.
The person put effort into the code, and I didn't feel that they were convinced of my argument against it. Their changes were more efficient logically, and I'm sure it would have saved a few milliseconds per call - whereas my claim of increased complexity was a more subjective judgement.
In the end, it came down to trust/authority - but they might have been more convinced, or felt better about the decision, if I could produce some numbers from my side, like a measurement of cyclomatic complexity based on static code analaysis.
Perhaps that could have justified the existing higher abstractions, to weigh their (almost negligible) performance cost against the value of reduced complexity - keeping the code base simpler to understand.
This is perhaps one of the most serious problems with software as an industry. More precise is that human investment doesn't scale, which includes: discipline, education, training, mentoring, and so forth. That is true for all industries.
Other industries know human investment doesn't scale, but even still it is absolutely professional and so it must still be accounted for. Software intentionally ignores this and then looks for things to blame when the tech debt becomes overwhelming.
> But along with the term of experience goes the breadth. You see, the person who learned reference counting in 1995 and thinks that you can only really understand programming if you manually type out your own reference-changing events, presumably didn’t go on to use garbage collection in Java in 1996.
I think that the article misses two points. In one side the more accessible writing code is, the better. To reduce the amount of knowledge and discipline that you need to code is good.
On the other side, discipline is still needed to design APIs, or just to maintain consistency between calculations and data as it moves thru your system, or keep technical debt in check.
> does that mean our tools should not let us write code for which there’s no test?
That depends on how your team/company wants to work. If you are doing Test Driven Development, it seems good that the tool forces you to do the test first. But, TDD is not the only or best way of developing software, so generic tools needs to support different paradigms.
I love code formarters for all this reasons. They relieve developers of the need to follow some basic code style guidelines, you use less self-discipline on writing good style and more on writing good APIs. But, to have a code formatter you need an agreed standard.
I try to make to write code easier for myself and other people, because implementing business requirements is already hard by itself. The more time you have to spend "coding" and the more you can spend thinking, the better.
Given the time and emotional effort I've given to hear out some of my colleagues' strong opinions on computer programming in the past, reading this felt a little cathartic.
There are two kinds of discipline; one scales, and the other doesn't.
Consider this line from the article:
> you don’t need TypeScript to write good React Native code, just Javascript and some discipline
The author provides this as an example of the "disciplinarian" approach, which the author says "doesn't scale."
It's an interesting example, because learning a new way to code requires discipline. IME, the main obstacle to TypeScript is that you have to have the discipline to learn TypeScript and endure the compile errors it generates.
I think you could rewrite the author's example the opposite way:
"You don't need to write careful JavaScript, you just need enough discipline to learn and use TypeScript"
But this is a different kind of discipline; it's the discipline to seek out a "better" (or at least more scalable) way of doing things. You typically hear static-typing fanatics calling dynamically typed languages "undisciplined."
TypeScript is an order of magnitude less popular than pure JavaScript, even considering just new code on Github. TypeScript may be better, but it isn't taking over the world--not yet, at least--because it requires (enforces!) more discipline to get started.
A more precise phrasing of the sense intended by the article would be that reliance on discipline doesn't scale. Encourage discipline, supply it as you're able, but recognize that when the concern is important it probably calls for additional measures.
Not making a mess of it is the most challenging task we face when working on complex or even simple problems. A disciplined approach is essential to not making a mess of things.
Yes of course, that is why 'special forces' in military are special, most of people are just cannon fodder.
Military orgs around the world are turning into smaller head counts of disciplined people. That is known since the Battle of Thermopylae but maybe getting lost from time to time. There is also a lot of military tech that is trying to prevent soldiers from shooting themselves in the foot, which fails from time to time.
We also know that just adding more developers to project is not increasing velocity and that small focused teams outperform large teams by orders of magnitude.
Problem we have to tackle are big orgs that need a lot of code and have high dev turnover. That is why frameworks like angular and tools first are so important, they help to scale code better than plain code or plain js.
If it's always the 'same kind of discipline' then of course what is needed is a higher level of abstraction so the programmer is freed from having to keep track of said 'disciplined parts' and focus more on other problems.
C++ while obviously useful, while it obviously 'works', and while it's existence in the context of history is also obvious ... is a giant clusterF of an anti-pattern programming language. The 'smaller, better language trying to get out' should have happened long ago, it didn't so maybe that will be Rust, but that's a big big leap and it's going to take many years yet.
Discipline is all what matters, automatizations are imperfect and temporary tools.
Simply put: all abstractions are leaky, all abstractions have trade-off, and you cannot necessarily rely on some abstractions in every context.
The GC example that the author uses is a good one: having GC in a language does not magically remove all memory allocation issues. Believing this means you just don't have much experience programming (or you've been very lucky). I've been working mostly with GC languages and we have memory leak every now and then, good luck to fix that without discipline. Embedded software frequently relies on (very) limited compilers and libraries, most of the time there are no GC and until recently there were even no malloc/free for $B things shipped to space: only static arrays and discipline my friend!
TCP is another good example: sometimes you need to craft you own congestion management, packet discard rule or what not.
ORMs have made my life easier but I've still had to spend days on complex hundreds lines SQL reports. The list goes on.
It's not "I had to go through this, you should too" out of frustration. It's because at some point the same kind of issue will appear again, even though the current available abstractions may seem to shield us from it.
It might not scale as easy as you wish, yet you need some disciplined seniors to review and teach your juniors. This very article ironically proves it :)
Different situations can call for different tradeoffs.
Because of Moore’s Law, technical advancements, and because of the increasing economic benefits of programming, this tradeoff has shifted to more automation over time.
These trends are still occurring.
As in automation of manufacturing, the tradeoffs are sometimes not as obvious as they appear.
I like the point the author is making. It appears that the examples themselves are talking of changes over long periods of time, that the programming community benefits from.
Are there examples where this applies to shorter time periods?
e.g. the discussion on the evolution of C++ language is fantastic, but it took place over.... years? A decade?
If I'm this person in my org trying to write better software for my project, does it really help to not be more disciplined? Before the invention of reference counting, wouldn't the developers who did know how the thing really worked be more productive?
I think this is important to call out, and not go to the other extreme. Discipline is definitely not scalable, but neither is ... the lack of it? Inertia is very strong in production software systems.
Starting the article with the history of “but you know what’s better?” just reinforces that there’s always a level where discipline is important. We will never reach the end of the “you know what’s better” cycle. So discipline itself, though not a particular discipline, is something a good programmer always needs. (Which is true of every other craft/skill as well.)
For the record, I do tell people they should learn C so they have a better idea of how computers work. That emphatically does not mean I want them to start writing actual production code in C, unless there’s an extremely good reason (and that hasn’t happened for me in a decade or two). Advising people to gain a deeper understanding of their tools is not “hazing”.
> does that mean our tools should not let us write code for which there’s no test?
I've heard not so good stories about 100% code coverage. Metrics do not scale.
Software development is a balancing act of visible results, technical debt, available workforce etc etc. Life (cells and above) is not perfect but it matches environment.
> no strong motivation to become more disciplined
Bug on production bug is much better motivation to write tests than some discipline. Write code so other people want to work with or search another place.
> either torpedo your whole idea or turn it into not doing the thing (see OOP, Agile, Functional Programming)
A lot of languages are not OOP by Alan Kay, Agile and SCRUM subverted. That is marketing, when benefits is in the badge not in the substance. Those who chased idea do well.
I would say that the validity of this argument depends entirely on the availability of hardware. To take an extreme example, reference counting would have been completely infeasible in the days of assembly programming. An assembly programmer who had this insight would have had to wait decades for the hardware to catch up. I'm sure more and more tools, language constructs and compiler features will emerge in the coming years to reduce the cognitive load on developers so that they can focus more and more on the business logic. But that doesn't invalidate the some of the discipline you need to make software fast and stable today.
I'm reminded of the blameless postmortem training we got john allspaw from etsy. "Try harder", "do better", "be more safe" are all discipline based solutions, and they don't scale. You have to outline specific steps, checklists, procedures, etc that will allow you avoid mental discipline and use processes. The same thing applies to dev work. IDEs, pipelines, scripts, etc, but also language design. Let type systems do the heavy lifting. You should shift as much from your head to a machine/process as possible. Less artisan, but more efficient.
Anyone who ever tried starting Lotus/IBM Notes cares about latency and performance now. I’m sure the JVM was great for the perceived developer productivity, but it doesn’t make a better product.
> And yet, for some people the problem with software isn’t a lack of automation but a lack of discipline. Software would be better if only people knew the rules, honoured them, and slowed themselves down so that instead of cutting corners they just chose to ignore important business milestones instead.
I do not think that memory is the biggest problem here, with RAII it "only" requires medium discipline, but UB is the dragon in the closet here. Avoiding UB requires a huge amount of discipline (or luck)
Hum, I think this person is missing the other side. It isn't that discipline will save you, it is that some programmers want power and control, and don't like restrictions. Those programmers do sometimes believe they know better, or just prefer and enjoy the creativity given to them. Thus any language that restricts you isn't something they favour.
There's an interesting comment below the blog from someone named Alex (not me):
> As a proponent of discipline, I see things a bit differently. You’re coming at it from the perspective of “In software, we automated X, and now X is no longer a problem, therefore, we should just keep automating as far as we can”. That’s one pattern.
> I’m seeing from the perspective of: “There’s no way to automate everything, and professionals always need discipline, so let’s start with that, and then see what else can/should be automated”. My pattern is from looking at the history of every other industry in the world.
> Discipline is the rule, not the exception. When cars crash, we test and license people, and issue citations for people who visibly break the rules, and increase driver education. We don’t ban manual transmissions (not enough automation!), or install breathalyzer interlocks in every vehicle. Self-driving cars may solve our remaining problems, someday, but that’s a long-term solution.
> Similarly, it may be possible in software to automate our way out of many classes of bugs (I’m skeptical), but that’s a long-term solution. Discipline is something we can adopt today.
> Two points you didn’t really seem to address:
> First, there’s diminishing returns on technology as preventative measure. Manual refcounting is way better than malloc/free. Automatic refcounting is quite a bit better than manual refcounting. Tracing GC would be a little better than that. Every problem is like this. When you’re down at the “malloc/free” level, advocating for discipline seems silly. (Why not just fix the bad tech first? It’s not hard to improve on.) But once you’re up to about the ARC level, discipline is clearly worthwhile. You don’t need to wait for a perfect tracing GC to advocate for social solutions.
> When there were only 10 cars in the whole state and no roads, licensing and education are probably not high on your list. But once you’ve got an Interstate Highway System, you need licensing, and education, and laws against drunk driving. Nobody in 1965 said “People are dying in car crashes — we’ve got to automate this by developing self-driving cars ASAP!” You look at the limiting factors. At some point, more technology is a huge cost, in time and money, and a social solution would be cheaper and more effective.
> Second, you seem to be using “discipline” to mean only an internal personal strength of will, but that’s not really what the word means. Yes, we know that relying on willpower alone doesn’t scale. The goals are quality and accountability, and in almost every other field, we accomplish this through laws, regulations, certifications, trade groups, professional associations, and so on. Those do work. Check your dictionary: the definition of the word “discipline” includes rules and punishment!
> Your surgeon is not performing a splenectomy while drunk, but that’s not because there’s a breathalyzer installed on the door of every operating room. We didn’t use technology to solve that problem. We used training, regulation, and (severe) penalties.
> That’s what software development needs today. We can’t keep pretending that more automation is going to solve everything. You’re not going to find a new “GC” level advancement every year, because there aren’t dumb inefficiencies like “malloc/free” still sitting around. Once the low-hanging fruit of basic automation have run out, there’s no substitute for professional discipline.
That is important point for arguing with people who say things like 'ORMs are bad I can write my SQL queries', 'Angular is bad I have my perfect approach for vanilla js'. When such person will have to explain their 'perfect' approach to 10 developers and then such person will have to keep doing code reviews so that approach is still used by those 10 developers it will be an eye opener
The encouraging thing today is that we're kind of converging on good practices in language design.
- You really want to declare types for function parameters. The compiler needs that information for checking, and the programmer, and the maintenance programmers who follow behind, need that information so they know how to call the thing. On the other hand, you can usually infer the result types of expressions by a simple forward process. So C++ gained "auto", Rust and Go started off with that arrangement, Javascript got TypeScript, and Python is getting there via a side trip through unchecked type declarations.
- The three big questions in C: "How big is it?", "Who can delete it?", and "What locks it?" C gives no help with any of those. C++ now tries to address the first two. All the garbage-collected languages address the first two. Rust addresses all three.
- The idea that locks should be connected by syntax with the data they are locking took far too long to become accepted. Partly because the Ada rendezvous and the Java synchronized object didn't work well. Even Go doesn't do that very well. But language design seems to be coming around. When and if thread-type concurrency comes to Javascript, the webcrap crowd will create race conditions everywhere without automation in locking.