I agree that GC languages are faster for prototyping, but this article downplays the disadvantages of GC. E.g. "deterministic destruction" is listed as a benefit of reference counting, but it really should be listed as a disadvantage of GC, since all the other memory-management methods have it.
The "leaky abstractions" argument doesn't work for me. E.g. in a Java API you don't commit (at the language level) to the ownership of passed-in or returned mutable objects. In Rust, you do, and the author claims this is a problem. I say that in fact you depend on the ownership of the referenced object either way; Rust forces you to write down what it is and enforce that the callers honor it, while in Java you cannot. Arguably therefore Rust is less leaky in that hidden assumptions are documented and enforced at the API layer.
I've had lots of good experiences refactoring Rust code. It's probably more typing than other languages but the increased assurance that you didn't break the code (especially in the presence of parallelism) more than makes up for it.
Probably the same for the others; in D you have RAII but destruction is only deterministic for stack/static variables. There isn't deterministic destruction for heap allocated stuff unless you count manually freeing.
That case isn't completely deterministic the way stack is: for the stack you just shift the SP which is constant time, while heap allocation and deallocation require manipulation of a small database, possibly including a system call.
Using the GC in D is entirely at the user's discretion. You can also do functional programming in D, or not. The same with OOP, or not. Or RAII, or not.
This is unlike Java, which forces the use of GC and OOP. Or Haskell which forces functional style.
> The "leaky abstractions" argument doesn't work for me. E.g. in a Java API you don't commit (at the language level) to the ownership of passed-in or returned mutable objects.
Yep, and so you get all sorts of bugs or defensive code everywhere. For that reason I also have a hard time with one section being
> GC'd code can be more correct
But apparently issues like iterator invalidation and other mutability conflicts don’t rate.
While technically true, I'm not sure that the iterator invalidation argument is that strong in practice. Rust doesn't seem to eliminate those problems, it more moves them around and transforms them into different (though sometimes easier) problems.
We often think that the borrow checker protects us from these kinds of mutability conflict / single-threaded race conditions, by making it so we don't have a reference to an object while someone else might modify it. However, the classic workaround is to turn one of those references into an index/ID into a central collection. In real programming, that index/ID is equivalent to the reference. But while I hold that index/ID, the object can still change, which kind of throws the benefit into question.
In other situations where we only have multiple read-only references, there's not really a motivating problem to begin with, even when coded in other languages that don't also regard them as read-only.
I think the other benefits the article mentions (concurrency benefits, plus encouraging into flatter, cleaner architectures) are a bit more compelling.
I get it about Rust, because Rust really won't let you hide the fact that you're holding on to a temporary reference (GC is an abstraction over lifetime of objects). Borrow checking is also intentionally less flexible about getters (allowing library author to do anything in the getter, but that makes exclusive-mutable getters exclusive over the whole object not just one field). So Java programmers are in for a shock — public fields are encouraged, and APIs have to commit to a particular memory management strategy.
But OTOH not having to worry about defensive programming is so great. Nothing is NULL. Nothing will unexpectedly mutate. Thread-safety is explicit. It's always clear when vectors and objects are passed, copied, or only briefly inspected.
Yeah, however even with using Scala/Clojure, there is the possibility to touch parts of the standard library that fail to uphold such expectactions.
So then one ends up with the usual issue of guest languages, where one needs to create wrappers for the platform APIs to keep the code idiomatic and language invariants.
The author mentions how experienced C developers use some techiniques that drastically improve the correctness of their code.
The same goes with Java: we never use mutability in modern code bases anymore unless we have strong reasons to do so and can keep that mutability as constrained locally as possible.
We also avoid inheritance these days, have your heard about that!? Because yeah, inheritance can be helpful but tends to bite you in the end.
> But apparently issues like iterator invalidation and other mutability conflicts don’t rate.
They don't, because you're using java.util.concurrent when you need data structures that deal with concurrency.
I miss java.util.concurrent terribly when I have to program in any other language. I don't miss Java, but I weep every time I need the equivalent of ConcurrentHashMap or CopyOnWriteArrayList in another language.
> in a Java API you don't commit (at the language level) to the ownership of passed-in or returned mutable objects. In Rust, you do[.]
That's only the default, of course. Rc is commonly used in Rust to decouple ownership management from the simple use or passing of objects throughout the program, much like in any GC language. (Of course Rc on its own does not collect reference cycles; thus, "proper" tracing GC is now becoming available, with projects like Samsara https://redvice.org/2023/samsara-garbage-collector/ that try to leverage Rc for the most part, while efficiently tracing possibly-cyclical references.)
Manual memory management does close-couple memory layout/ownership with the public API, and that will objectively make refactors regarding that layout breaking changes, causing many downstream changes required.
A quote from the GC Handbook: “modules should not have to know the rules of the memory management game played by other modules”
While Rust does make these refactors safe, it is nonetheless work that simply doesn’t have to be done in case of Java for example.
I disagree. It depends on the type of project you're working on. If you're writing a public facing high-performance application it MAY be more beneficial to use deterministic memory management.
Most tools, even very technical tools like assemblers, debuggers and compilers, don't need deterministic memory management. Only operating systems, device drivers and some time-critical applications (like video/audio encoding/decoding) need deterministic memory management.
The trick is, when you have deterministic destruction you can use it for many more things than just memory management. E.g. need to add a metric to measure how many "somethings" are active - just attach a tracker to the struct representing that "something" and the tracker can count itself because it knows exactly when it is created and destroyed. We tried to add such metrics system to a Java app another day, and it turned out to be virtually impossible because due to GC nondeterminism we couldn't tell when most of the objects die.
Do you really need to know? All that matters is that the GC will eventually clean up the used memory. When you're interested in that sort of info you're using the wrong language or asking the wrong kind of questions.
It comes up quite a lot in things like databases and high-performance data infrastructure generally. The instantaneous resource state is an important input parameter to many types of dynamic optimization. Ideally, you want to run close to the resource limits without exceeding them for maximum throughput and robustness while minimizing tail latencies. Knowing precisely how many resources are available allows the code to opportunistically apply that capacity to workload optimization without risking resource starvation. Some software will also adaptively switch entire strategies based on this information. It is very useful information if you are designing for performance.
If you have no idea how many resources are actually available, you are forced to use conservative strategies to protect the process. Guessing or ignoring resource availability is an excellent way to greatly increase performance variability and reduce throughput.
Yes, of course I need to know. An important part of production grade software is observability. And that "something" doesn't have to be memory. It can be a connection, file handle, semaphore permit or one of many other things. But even for memory, typically there is not just one global type of memory. Users want to know how much memory is dedicated to X, Y and Z components, they might also want to constrain memory use by logical function. E.g separate buffer pools. This is where automatic memory management with GC falls apart very quickly - even the Java apps I work on actually use a memory/resource abstraction layer on top and manage everything manually, leaving just the last bit of releasing the objects to GC. Which is the worst of both worlds - we get all the disadvantages of MMM and all disadvantages of tracing (pauses, cache thrashing, etc).
Java doesn't sound right for your use case. Most programs written in Java works just fine without tracking memory, and if you have any issues you do a jvm memory dump to see which object types are taking up all that memory and then look at why they weren't collected.
Sure, I know that. But sometimes you don't have a choice because the decision was made years earlier by someone else, and you obviously won't rewrite a 1M LOC codebase.
Nevertheless, after spending a year writing Rust, I'd take Rust over Java for just any use case now, even including CRUDs and GUIs (actually doing a GTK GUI for a Rust app right now, and it is not any worse in terms of dev speed than Java Swing was, despite what people say about callbacks - I really don't mind an occasional Rc/Refcell/clone here and there).
>> need to [...] attach a tracker to the struct representing that "something" and the tracker can count itself because it knows exactly when it is created and destroyed.
> Do you really need to know?
He, uh, just said that. If you're tracking profiling metrics like he is (lifetime of a call-stack, maybe?), you need to know.
> When you're interested in that sort of info you're using the wrong language
Under that argument, Java is the wrong language for everything.
> The "leaky abstractions" argument doesn't work for me. E.g. in a Java API you don't commit (at the language level) to the ownership of passed-in or returned mutable objects. In Rust, you do, and the author claims this is a problem. I say that in fact you depend on the ownership of the referenced object either way; Rust forces you to write down what it is and enforce that the callers honor it, while in Java you cannot. Arguably therefore Rust is less leaky in that hidden assumptions are documented and enforced at the API layer.
A lot of programs and large parts of a lot more programs just don't need these concepts at all and would run correctly with a GC that did not collect any garbage, memory permitting. Most Python code I write is in this genre.
On the flip side, at work we mainly work on a low-latency networked application that has per-coroutine arenas created once at startup and long-term state created once at startup. The arenas are used mostly for deserializing messages into native format to apply to the long-term state and serializing messages to wire format. There's not much benefit to be had from writing all the time that everything that isn't on the stack belongs to the per-coroutine arena, except for all instances of like 3 structs, which belong to the long-term state.
As an older dev who spent a lot of time with C on applications where performance and timing was key, I have found the timing of the overhead of GC hard to predict. So I tend to pre-allocate a lot of variables and structures in advance and hold them, so I don't have to worry about GC in a loop. If someone knows a good tutorial of how to predict when garbage collection will happen in my program, I would love to read it and learn more.
It is more typing than julia or numpy maybe, but probably not more than c#, maybe not more than go, and most certainly not more than java.
I think even within GC languages developer velocity varies a lot based on things like typing system, reflection, repl, and maybe interpreted vs compiled. My experience is that this variance is greater than what can be put at the feet of memory management
I haven't written much go or much java (in this millennium anyway). The little go I wrote didn't have many error checks, is that what usually makes it verbose? The modern java I wrote was a little more functional than 1999 java, but it utilised reflection heavily, and it was a test harness for a black box, so not idiomatic probably, but there was still a lot of boilerplate. C# seems about normal verbosity, and so does rust. Both are less verbose than a lot of c++98 I run into because of lambdas and iterators etc. Julia on the other hand is quite possibly the least verbose language I have ever seen, apart from matlab (which isn't truly general purpose) , and python/numpy/pytorch close behind.
Even if you use a non-GC language, you are not guaranteed deterministic destruction (e.g. in data structures with loops or a highly variable number of nodes).
Deterministic is not the same as bounded. Deterministic means you can determine the moment the destruction happens and you can estimate its upper bound (knowing how much you want to release), not that it always happens in 100 ns. You also get a guarantee that when a certain point in code has been reached, then a resource has been freed.
As for the cost it takes to release the resources, in MMM/borrow checking/refcounting this is typically bounded by the amount of stuff to release. In tracing GC it is bounded by the total amount of stuff on the heap, which is typically several orders of magnitude larger value.
This is a really interesting observation: "Stateless programs like command line tools or low-abstraction domains like embedded programming will have less friction with the borrow checker than domains with a lot of interconnected state like apps, stateful programs, or complex turn-based games."
In my book [0] teaching Rust, I manage to completely avoid using lifetimes. Perhaps this is because it reimplements a command line utility (head, wc, cat, ...) from the original BSD sources in each chapter and none of them are stateful. Most of the classic Unix utilities just process streams of bytes and barely use data structures at all. Many don't even call malloc().
It feels like Rust is a good match for the problem domain but I never made the connection with the stateless nature of these tools before. This bodes well for eventually replacing the coreutils with modern Rust implementations.
This was a particularly fascinating realization of mine a few years ago. I realized that for certain domains, the borrow checker was a dream to use (command line tools and scripts, ECS games), and in others it wasn't so much (GUI, turn-based games).
I dove a little deeper, and the stateful/stateless distinction seemed to be the best rule-of-thumb for predicting how much one would be in conflict with the borrow checker's preferred styles. It was also reminiscent of functional programming languages, in a way.
I suspect this is why most discussion comparing Rust to other languages devolves so quickly, and has become so polarized: it depends on the domain. Users who try to use it for some domains will hate it, and users who try it on other domains will love it.
This is also why I recommend newer Rust programmers to not be afraid of using Rc/RefCell in certain domains. Some domains don't agree as much with the borrow checker, and Rc/RefCell can make the architecture a lot looser and easier to work with.
For 98% of tasks Rust is incredible. But modeling those 2% of problems that don't fit neatly into Rusts domain, the level of "unsafe", "PhantomData", and advanced type-system hacks you need feels obscene when I can use some pointers in C++.
I will openly say I think Rust is an all-around better language. If you are building general-purpose systems software, or just want to write fast software, use Rust.
If you are fiddling with bits, doing low-level concurrency, writing a GC/Memory Manager etc, or have self-referential datastructures like graphs or trees, you're going to have an order of magnitude easier time writing it in C++. (Rust-experts excepted)
> But modeling those 2% of problems that don't fit neatly into Rusts domain, the level of "unsafe", "PhantomData", and advanced type-system hacks you need feels obscene when I can use some pointers in C++.
I don't agree. This happens only if you want to not make any compromises on the performance and stay on the safe side, which is a goal unreachable for the majority of languages. Often an Arc/Rc/Refcell + a few clones makes the borrow checker shut up and the code is not more complex than it would be in another GCed language. And often still faster (although YMMV), because those few clone calls may totally not matter, and Rust compiler runs circles around compilers/runtimes used in most popular GCed languages like Java, Go, JS or Python.
You can also use raw pointers which is not different from using raw pointers in C++. I have no idea why some people say using pointers in C++ is fine but writing some parts of a program in unsafe block in Rust is a problem.
> I suspect this is why most discussion comparing Rust to other languages devolves so quickly, and has become so polarized: it depends on the domain.
The problem is that Rust proponents argues that Rust is better for all domains. I don't think anyone says that Rust doesn't have a place, the source of controversy is whether every low level programming task is best done in Rust or not.
Not sure if it is strictly better, probably not, but it is actually damn close to being better in most domains and not worse in the others. I've been developing a GUI app in Rust, and even though this is an area considered widely a very bad fit for a borrow checker and a non-OOP language like Rust generally, the borrow checker was never an issue for me (and it still helped in a few places to get e.g. concurrency right). An occasional Rc/Refcell here and there does not outweight the other advantages. Definitely not any worse than Java Swing in terms of productivity, but way better in terms of the end result (which launches in 0.1 seconds, has no visible lags and uses native window controls).
Memory isn't the only resource that needs management. File handles, network sockets, locks, security codes and more, all need to have deterministic lifetimes. If you language doesn't support lifetime management via some mechanism then all of these things are the next part that will bite, even if you have GC for memory.
Try-with-resources is tied to a lexical scope. C++/Rust RAII is not, you can move the resource e.g to another thread after initialization, and that thread can outlive the scope that created the resource. Also C++/Rust allows shared ownership through recounted references, something try-with-resources can't do.
Cleaners aren't deterministic, so that's meaningfully worse than rust's resource mgt.
Java's try with resources cleanup only works if the resource is freed up before the callstack is unwound. Currently that's only viable for the most trivial of scenarios. Loom will make that applicable in more places but still not as widely applicable as a borrow checker/rc based deterministic resource management.
What about if you want your resource to be longer lived, but still safely disposed of in a timely manner when it goes out of scope, for example a port in a webserver class? Do you have to keep the stack alive here to keep the resource, or can you put it into an object somewhere to get a more RAII style resource management?
Though there was nothing inherent to ever block you from doing it (just create an object that implements Closeable, and malloc in the constructor, free in the close method), this new API makes it quite great. The basic API gives you a SegmentScope which denotes a lifetime, and MemorySegments that point to a memory region (pointer+size+layout+scope). MemorySegments have both spatial and temporal safety, so accessing memory out of boundary will only throw an exception instead of corrupting memory, while any access outside the lifetime of the scope is forbidden. Oh, and they are also thread-safe, only optionally being shared between threads.
So in practice it you can write something like
try (var arena = Arena.openConfined()) {
MemorySegment segment = arena.allocate(someLayout);
// use segment
} // memory will be deallocated here
Java and the JVM has a garbage collector, and will likely always have, and GC makes memory management a lot simpler and safer, so while you could use try-with-resources for some kind of memory related resource, it likely would not be to make things safer, but to make things less safe and faster, and if you start doing that you will have significantly reduced safety compared to rust I think.
It is, I've written c++ as my primary language for 30 years. I skipped c++14 and I'm still using 17, but staying up to date other than that. But RAII doesn't come anywhere close to the borrow checker in the number of mistakes it prevents.
I write most of my code in Nim language, and it generally involves wrapping up some C codebase, or calling DLLs routines and working with raw pointers. Although Nim has it own GC or ARC to handle memory management for its native data-structures, i find it really helpful to define a `destructor` for any arbitrary objects/structs. Since language can track lifetime of any object/struct during compilation and can call corresponding `destructor` for that object, which generally is just a `free()` or `some dll exposed routine`. I end up using language to handle all the manual memory management even for existing C codebase.
With new ARC, all destructor calls are inserted with compilation and hence no extra GC based runtime to deal with, and even works flawlessly with GPU programming stuff too !
I haven't used many programming languages, but personally find it really useful to use any language as an assistant to help solve issues outside of their intended context.
> In an average Google Earth quarter, only 3-5% of bug reports were traceable to memory safety problems, and Address Sanitizer made them trivial to reproduce in development mode.
Bug reports, sure. But as for security vulnerabilities, both Microsoft and Google have reported that roughly 70% of them were memory safety issues.
Personally, as a Rust fan, I fully agree that the borrow checker limits developer velocity, and I'd be happy to toss it if I could. But verdagon has been promising memory safety with no overhead for years; I'll believe it when I see it.
Edit: Okay, that was unfair to say without further context, so let me add two things. First, although Vale is open source, the design has changed over time and the implementation was described as a "work-in-progress" as of two months ago. [1] Second, a month before that, verdagon described the design as providing "statistical safety", meaning use-after-frees may fail to be caught but are caught "99.9999999999996% of the time". Now, stochastic exploit mitigations such as ASLR and PAC are valid defenses, as long as that 99...% also applies in the presence of an active attacker. But the specific design described (allowing generation counters to wrap) sounds like, in many scenarios, an attacker could manipulate it to provide no protection at all. There may be ways to mitigate this, but in context it does sound like it's designed under the assumption that memory safety violations will occur at random, rather than being designed to defend against attacks. This might be good enough for (some) games, but not for most Rust use cases.
>> As you can see, software development can be much more expensive than power usage, so it makes sense to primarily optimize for development velocity.
No? Those developers can take time off, but the electricity can not. I think he showed how important optimizing software performance is at scale. When you electric bill is $3Billion, you gotta write efficient code, not write more code as fast as possible.
That example threw me off too because both the numbers and the perspective are non-nonsensical. 90% of the energy draw of those data-centers goes into things like inner video encoding loops, SSD/memcache storage and retrieval, ML algorithms etc.
But the vast majority of those 27.000 engineers do not work on such low level routines, but on things like millions of lines of crufty Python that power Adsense analytics, which are essential for maintaining a revenue stream. Yes, development speed is very important but it's mostly orthogonal to other operational costs if the right tools and architectures are employed.
Author here, that might actually make the comparison stronger: not all energy in a data center is wasted by the memory safety approach's drawbacks, meaning it makes less sense to optimize for memory safety overhead.
But if I'm being pro-Rust, I would also say that not all coding is affected by a memory safety approach's downsides; there are some domains where the borrow checker doesn't slow development velocity down at all.
Either way, I definitely agree that its orthogonal to many operational costs. I'll mention this line of thought in the article. Thanks!
So this particular problem is severely time-constrained. Performance doesn't relaly matter. In those situations you tend to use the highest level language you can. This isn't surprising.
The author argues it's similar for startups. I agree to a point. For GC in particular I think the costs are underestimated. Processes blowing up because of memory leaks (which can still happen), dealing with stop-the-world ("STW") pauses and inconsistent performance are all real problems.
So much of this is situational. For example, I think Objective-C/Swift was so successful on iOS (vs Java on Android) in part because it opts for reference counting instead of GC. This is predictable performance and less complicated. But RC isn't necessarily appropriate for a server as you may create hot spots and degrade performance that way.
Rust by virtue of borrow checking isn't a panacea. It does however greatly reduce the chances of making a whole class of really important bugs (ie memory safety and all the entails like buffer overruns). Comparing it to Python, Go or Java doesn't necessarily make sense. Rust lives in the same domain as C/C++.
One funny side note:
> Google used 15.5 terawatt hours of electricity in 2020
Bitcoin used 200 TWh in 2022 [1]. Think about that.
One of the interesting things about MMM and borrow checking is that it enables you to implement GC and RC. Rust has RC built in of course, but the general consensus seems to be that it's "not idiomatic". There's also crates for GC. Would a Rust project that relied heavily on GC be able to sustain the sort of velocity that a project in a native-GC language had? Would it even be close?
I love Rust's developer experience, frankly. The macros and build system are the main reason I like the language. I hate the borrow checker most of the time, for exactly the reasons listed in the article. It's probably a terrible choice for a 7DRL project, unless it was some sort of Rust core with content that was primarily orchestrated from another language (possibly DSL).
I’m not sure regarding the benefits of adding such a library-GC (that effectively does RC with cycle detection) to Rust. RC is just very slow and there is a very good reason that no performant managed runtime uses it. They are good in that they need no explicit support from the runtime, but that only makes them possible, not performant.
I would wager introducing a small rust hot loop via FFI into a managed language is a much saner route, using the respective languages to the best of their abilities.
Obligate ARC is slow. RC/ARC as used in Rust (ARC separately marked, only used when required for thread safety; RC in general only for objects that may need to be kept around by multiple parts of the program, with no fixed "owner"; with simple RAII used for everything else) is very fast, comparable to manual memory management.
It can remove a few increments/decrements (more like a single pair at inlining a function), but that’s not what makes it problematic.
If you do have a bigger object graph, not using it anymore will recursively go on and decrement the counters, removing potentially many objects from memory, totally killing the given thread’s cache/performance. All that is amortized with a tracing GC. On top, atomic counters are very expensive.
Of course you can probably get away with using (A)RC at few places at most with Rust, but if you decide to track object lifetimes with RC for loads of objects, it will have a price (which may or may not be worthy. In my opinion, it is better to stick to Rust for low-level programs)
Makes sense from a performance standpoint. But what about development velocity? If the hypothesis is that wrestling with the borrow checker is a big drain on productivity, then bypassing it should restore that productivity.
> I love Rust's developer experience, frankly. The macros and build system are the main reason I like the language
On the contrary, I hate the macros, because the formater can't format inside tokio select or some of the other macros, and that means I have to do it manually, which is lame.
That's a pretty weak reason to hate macros, frankly. Seems more like a deficiency in the formatter than in the language feature.
As a side note, there's a poorly-documented behavior where rustfmt will format macros that are wrapped in {} if it's valid rust code. So you can even point the blame on tokio for not using formatter-friendly macros.
I dislike them primarily because of the additional complexity they add that isn't always justified. All of the more complex macros I wrote I regret, especially the one I did at my last job since now others had to maintain it.
Some macro use can be justified, eg serde. But I try to avoid using proc macros where I can (and certainly never try write one anymore)
> In other words, CHERI can reduce memory-unsafety related slowdowns by two thirds, which is pretty incredible. AMD CPUs are even starting to have hardware support for it, bringing its run-time overhead down to 6.8%.
Source on AMD CPUs having support for CHERI-style capabilities ? Afaik, there is only the Arm Morello prototype out right now and FPGAs.
I don't think this article is that interesting because it's really biased, and doesn't even try to catch any kind of nuance, especially regarding Rust.
Developer velocity in Rust is a complex topic, and yes sometimes Rust will slow you down, but sometimes also it makes you move much faster. You can find a lots of testimonial in all directions, the majority of Rust users having a good opinion on Rust on that topic (but we're biased too). But somehow this article only chose to quote people complaining about developer velocity with the language and completely ignore the other (and larger) group of developers, praising the language user experience and development velocity.
I don't think Rust is perfect, and I think that people talking about how “Rust is the most loved language on Stack overflow for the past N years” isn't really constructive. But a collection of cherry-picked quotes showing how Rust is in fact a disaster isn't bringing anything interesting to the discussion either…
Author here, I tried to write this article to be as non-biased as possible, though it was pretty difficult.
Most discussions and sources online compare Rust to easy targets, like C, C++, or Python, so we never get to really dive into the more interesting comparisons against stronger languages (especially GC'd ones). There are also some studies out there that try to measure Rust, but they measure the kinds of complexity that wouldn't translate to real-life coding.
I actually like Rust (it's probably my favorite mainstream language today besides Scala), and if you consider all the dimensions together, there are a lot of situations where Rust is the best choice. However, this article is about one specific dimension (developer velocity) and on that, Rust unfortunately has some drawbacks.
Also, the article does mention situations (concurrency) and aspects (encourages top-down, flattened architectures) that give Rust some advantages.
I tried to be un-biased. Perhaps it didn't quite show, also since the conclusions recommended languages other than Rust when focusing on developer velocity.
I mean, that's OK to express criticisms on Rust developer velocity (for instance, I totally agree that Rust doesn't particularly shine for prototyping) but then you've assembled from various parts of the internet a collection of TWENTY-ONE testimonials from people complaining about developers velocity in Rust, and not a single one of people praising Rust for the productivity boost it offers, though I can definitely assure that there are, especially for refactoring where Rust is commonly considered[1] to be first class.
Collecting pain point can be useful for the language, as it helps the Rust team to get a better vision of what they can improve, but doing such a one-sided list in an article about comparison to other languages and memory handling mechanism is at best dubious.
> Most discussions and sources online compare Rust to easy targets, like C, C++, or Python
My background is mostly Java then JavaScript and you can quote me on the fact that I found Rust to be more productive than both of them in average, especially when the project last more than a few weeks. The biggest underlying reason is different though, JS mostly suffering from dynamic typing + overall questionable language and “standard library” design, while Java is plagued by inheritance and dubious OOP patterns. When using both of these languages after Rust I also felt frustrated by the impossibility to assign strong ownership to objects, making sure they aren't being watched or mutated behind my back. Also, billion dollar mistake.
[1]:by rustaceans that is, it's not an objective measurement of any kind, but we're talking about testimonials here.
Like I mentioned in response to your other comment about this, the anecdotes were added to the borrow checking sections because that's what the initial readers were surprised about. That in turn made those sections longer. I also did mention the benefits of borrow checking (such as the concurrency benefits and influencing us into cleaner architectures), plus the downsides of garbage collection.
I didn't need any anecdotes for the borrow checker's benefits because everybody already knows them. I see how that can seem biased though, and next time I add citations to the surprising parts of an article I'll also add them more uniformly to the rest of the article as well.
Also, the article was about garbage collection itself versus borrow checking itself, not any specific languages that use them.
You make some valid comparisons between Rust and Java and Javascript, but garbage collected languages don't need to be dynamically typed, and don't need to have null, and don't need to have OOP patterns. When you compare Rust to a more modern language like Scala or Pony, you get a much truer comparison of the approaches.
That's what the article is really comparing: borrow checking versus garbage collection. Not the extra features that are correlated with them in mainstream languages.
Is biased and incorrent in a few places, like disregarding system programming languages with GC that also support deterministic destruction of resources.
I'm not surprised: when you read something and the part you know about is bullshit, there's a good chance that the part you don't know about is also bullshit even you can't see it.
What language have both GC and deterministic destructors, and how does it works? I'd guess it couldn't be tracing GC (because the GC runs whenever it wants, so you don't have determinism, or at least that's my understanding). Or maybe it's something like RAII for non-memory resources (with a destructor that doesn't free memory), and a GC for memory management?
Mesa/Cedar, Modula-3, Active Oberon, D, Nim, for example, not to make an exhaustive list. C# also does provide some mechanisms to dive into it, since version 7, although not as easy.
Basically you can use GC heap as usual, or native heap with untraced references, stack or global memory segments.
Value types with constructors/destructors pairs are also supported.
So you have all the building blocks to do C++ style RAII when needed, otherwise relax and enjoy the GC.
This article was mainly about memory safety approaches (GC, RC, borrow checking, etc) and their effects on development velocity. It wasn't trying to compare all languages along all dimensions. That would be quite a long article!
Anyway, I think you're referring to `defer`? Though perhaps you're referring to something else, as defer also appears in non-GC'd languages as well so it didn't seem like a very relevant factor in this specific comparison. I do agree that defer (and try-with-resources) can be stellar tools for GC'd languages.
How do you know that? Is there any data backing that up?
I've never seen any research showing that a programming language, no matter how strict (Haskell, Ada, Rust) actually improves the reliability of software, except for comparisons between memory-safe and non-memory-safe languages. It almost always goes down nearly entirely to development process and team skills/experience, showing anything else convincingly would be a huge breakthrough.
Well, here's my point of anecdata: despite Python being a very productive language and very easy to use, despite it existing for more than three decades and being perhaps the most massively learned language by aspiring programmers today, you hardly see any stand-alone application written in it. I can name not a single application that I use on a daily basis written in Python. It seems it's strongly confined to the web-server, where the environments are well controlled via containers, runtime bugs impact a single page load and fixes can be applied continuously. People don't ship Python standalone apps.
Based on the buggy and unstable Python desktop apps I have used, I have a strong suspicion that developing large applications in Python is strongly self-limiting after the initial sprint.
I'm speaking from my own experience here. I've tested a couple of Rust programs which were developed in relatively short order yet were quite complex (a Minecraft type game being one) and there wasn't even the slightest hint of instability.
Some of my own Rust code is moderately complex but never showed any signs of instability during development. I often have crashes now and again with my C++ programs. Sure, I fix those afterwards but getting it flawless every time the first time is (for me at least) unheard of.
I'd agree with that too, but that's just my point of view and I'd respect anyone else's sensible argument of the opposite.
But this article isn't that, and to make manual memory management more appealing they had to ridiculously inflate the issues that come with ownership-based memory management…
I think it's you being biased against the article now. As I read it, it was pretty honest and really trying to find some sort of true, not just trying to bash Rust. Feels like Rust fans are very defensive against criticism of any sort, even when well intentioned as I think is the case here.
Author here, I tried to make this article as balanced as possible, and even talked about Rust's advantages in concurrency and encouraging cleaner architectures and the disadvantages of MMM and GC approaches (plus other aspects, see my other comment down-thread about this).
There are actually 45 citations in the article on all angles, but I think you're talking specifically about the anecdotes.
Regarding the anecdotes, I had to add more of those to the borrow checking sections because it was the most surprising to my initial readers. Very little discussion online actually compares borrow checking to higher-level languages with good development velocity; most discussion online compares it to languages like C, C++, Javascript, or Python, so this was new to most readers.
The article also explicitly mentioned that those were anecdotes and colored them differently, so that people didn't mistake them as data.
They also made that part of the article much longer than it was originally.
I can see how that could come across as biased. Perhaps I should have added citations to the other parts of the article so their distribution was more uniform.
When you look at the content itself, it's pretty balanced I'd say (hence the focusing on the other benefits of borrow checking plus the downsides of GC), it's unfortunate that's not coming through as much.
It may slow them down during their initial development when they're new to Rust, but I'm convinced that it will be much cheaper during the entire Application Lifecycle (ALM), not to mention customer satisfaction.
Developer velocity is a crock. You want to write code that actually works over the entire software lifecycle, not just crank out piles of utterly broken sh!t that you'll have to fix later at huge cost. Rust has it right, and Ada/SPARK even more so.
If you know algorithmically what will work sure, but if you don't, and you need to throw multiple approaches at a problem until one sticks, then a prototyping language is extremely helpful. The problem comes when management notices the prototype mostly works, they want you to ship it rather than make something sane now that you know which direction to head. I find using matlab is useful for this. Managers seem to understand that you don't ship matlab, where if I used numpy or julia...
This is interesting. It strikes me that if you think that you know a better language for writing a program than whatever your organization uses that there is nothing stopping you from writing your prototypes in that language and then porting them back to whatever the org uses when you're done. This is similar to the technique of writing a grammar with a parser generator and when you're happy with it writing a custom parser by hand.
This article is biased in more than one way and misses a lot of nuance and aspects of GC-based languages which are not limited to pure GC and employ a lot of techniques for deterministic and semi-deterministic memory management, and various trade-offs to optimize for single/many-core and throughput/latency scenarios. In fact, many modern garbage collectors are not dissimilar to arena allocators making most of the allocations very cheap and efficient.
In addition, Rust is difficult to use for iOS as it stands today but you can for sure make it support a variety of scenarios with delegates aka callbacks/function pointers/etc. Also RAII can be supported with .drop() and more. Half of the cons are incorrect and misrepresent current state of affairs.
It also recommends defaulting to Go over C# for making a web server which my religious views compel me to publicly object to :)
Author here, you're correct that there are a lot of performance tradeoffs involved with various GC'd languages, and also correct that GC'd languages do have some support for deterministic destruction (especially with defer).
The article was trying to focus on the general approach of GC more than the GC'd languages themselves, and the article was already pretty long so I tried to keep it scoped to just developer velocity implications of the memory safety approaches themselves.
I did mention some of those aspects, for example how C# is more than just a GC'd language (it has value types to help avoid GC), and Rust is more than just borrow checking (it has RefCell). But I couldn't get as deeply into these other aspects as I would like.
(It's also pretty funny that the article's been called biased by both sides, in both directions now! Such is life.)
I also don't mean to specifically recommend Go over C#, and I see the line that gives that impression, fixing now.
Interesting (though clearly biased) article. I suppose we shouldn't be too surprised about the bias given the source.
My main issue with it is the focus on short term developer velocity. Fine if you are writing a game in a week and then stopping. Most people are not doing that.
It has a great list of interesting new languages to look at!
That being said, Rust has taught me that I was vastly overusing graphs (structurally, not algorithmically) in my code. Graphs are extremely difficult to reason about; a fact that I learned after approaching an unfamiliar Rust codebase for the first time. It was the brain equivalent of putting down a screwdriver and picking up a powertool, my mind just relaxed.
That alone is one reason I think that every developer should try and "suffer" under that constraint for at least one or two real problems (whether with Rust, or some other language). You'll end up writing better code in your preferred language.
The removal of a feature is sometimes a feature. While roads limit where you can drive, they also mean you don't have to figure out how to traverse a mountain in a Lambo.
That is how you write programs in competitive programming, just put all the data you need in a nice table structure and use indexes instead of references. It is very readable and easy to reason about for simple programs and is extremely fast to write and performant, that is how you are able to implement novel algorithms in minutes. But it doesn't scale, indexes aren't type checked nor do they tell you which collection they point towards.
But yeah, knowing how to program like that is useful, learning new style will never hurt, but being forced to code like you code in competitive programming isn't a good thing for a language.
"Rust has made the decision that safety is more important than developer productivity. This is the right tradeoff to make in many situations — like building code in an OS kernel, or for memory-constrained embedded systems — but I don’t think it’s the right tradeoff in all cases, especially not in startups where velocity is crucial."
I completely disagree with this statement. If a Rust developer takes longer to code some feature this time will eventually be saved fixing memory bugs later on. And don't forget the immaterial cost of losing customer confidence if the product crashes or glitches because of memory instability.
And then I'm even glossing over the enormous benefit of Rust when writing correct multi-threaded code, which is almost always a minefield in C/C++. Code that looks and seems to work OK might in production suddenly crash after a year or so. A complete nightmare!
My take is that it was always about expanding the pool of people capable of writing safe code, thus making it cheaper.
I spent most of my career in front-end, where such considerations are way down the priority list, but even I was able to produce something in Rust that compiles.
That being said I never understood why would anyone want to use this language for web development - most problems are solved in that space, so if you're not out to tackle tje unsolved ones, it might not be worth it.
> That being said I never understood why would anyone want to use this language for web development
On the backend, at least, Rust would be a significant improvement over Ruby, for example, once a business has gotten past the initial prototyping phase. I've spent years in Ruby development, and the duck typing makes refactoring a production system scary and drama-prone. No matter how carefully you proceed, there will be gaps in test coverage, and you'll regularly see new bugs in production from the refactoring work that would never happen in a typed language. I would gladly work in Rust over Ruby in a web development context. (Although my preferences lean towards Rust, there are no doubt many other typed languages that could do a good job here as well.)
> Lobster is using borrowing and other static analysis techniques under the hood to eliminate a lot of reference counting overhead.
Nim is one of the first languages to use borrow checking to prevent copies and ref counts, and it's done both explicitly with the experimental 'views' feature and when permitted implicitly.
> Aliasability-xor-mutability is just a rough approximation of the real rule, dereference-xor-destroyed.
This is true in a single-threaded context, but in a multithreaded (or interruptible) context, aliasing-xor-mutability is the real rule for everything other than atomics. Breaking that rule is a data race, which is per se UB in C/C++/Rust/Zig/Go/Swift.
Author here, Cone is a pretty fascinating language.
It builds a borrow checker on top of any user-specified allocator, whether that be single ownership, reference counting, arenas, garbage collection, or any other user-specified strategy. A user can even code their own.
It's a very promising approach, because it preserves the borrow checker's strengths while addressing its the borrow checker's development velocity downsides. GC and RC often make a program's overall architecture looser and more flexible, but by offering borrow checking for everything else, it allows a program to much more cut down on its memory safety overhead.
What Cone does differently than other languages is that it decouples the allocation strategy from the type of data you're working with. This makes it much easier to change your code to use different memory safety styles. In a way, it's like Rust but allows for more flexibility on memory management.
It's still in progress, but I encourage anyone interested in languages and memory safety to take a look: https://cone.jondgoodwin.com/
Technically it’s only the standard library that lacks custom allocator support. Box and Vec and all the rest are “just” types defined in the standard library, and if you define your own, you can use any allocator you want and still take advantage of the borrow checker and other language features. This has been possible since 1.0.
…Well, except for certain magic features that the standard library types get that you can’t (yet) replicate in a custom type. This is an annoying wart, but they’re relatively minor.
But yeah, custom allocator support for standard library containers is planned.
I recommend taking a look at Cone in a little more detail, its type system support for allocators are head and shoulders above what Rust has. The language's awareness of allocator allows it to decouple it from the users code in a way no other language allows (except perhaps Odin).
I think Rust is missing a fair bit more than "user-specified local allocators" to be on feature parity with C++. Curious if it plans to be on full feature parity.
It definitely misses some features that are considered bad: e.g implementation inheritance or memory handling by exceptions. Those are not essential for anything and are not planned.
What authority declares implementation inheritance bad may I ask? As long as one does not stick it into inappropriate places it is actually very helpful.
1. it can be always replaced with composition, which Rust already supports
2. it comes with a non-negligible complexity cost to the language (e.g. what if you do multiple inheritance, what code gets called at object construction etc)
You probably don't want to end up with a language that implements all ideas that are good in some context. Especially you don't want to have different mechanisms for achieving the same thing.
>"1. it can be always replaced with composition, which Rust already supports"
This is no argument from my point of view. Everything can be replaces with simpler things. We are not going back to assembly however except very few cases.
"2. it comes with a non-negligible complexity cost to the language (e.g. what if you do multiple inheritance, what code gets called at object construction etc)"
Every feature has complexity costs and I prefer to have options here. It should be me who decides what I am willing to trade and what for.
>"Especially you don't want to have different mechanisms for achieving the same thing."
They're not exactly the same thing and actually this is exactly what I want. I am very averse of super opinionating "my way or highway" type of things. Oh and btw from a glance Rust seems to have different mechanisms for the same thing too, for example with all those option / unwrapping / question mark.
The question was asked about "by what authority". Your arguments are just an opinion not any better than mine.
> it can be always replaced with composition, which Rust already supports
Strictly speaking, open recursion (a notorious footgun in implementation inheritance, and arguably the underlying cause of the "fragile base class" problem) cannot be implemented via composition as-is. You need a complex "tie-the-knot" construction, typing "self" as an existential type defined in terms of itself, to enforce an indirection through the object's vtable in all calls to overridable methods. (AIUI, Rust does not have existential types that are general enough to allow this, albeit that might happen at some point as part of overall improvements to the type system.)
The "leaky abstractions" argument doesn't work for me. E.g. in a Java API you don't commit (at the language level) to the ownership of passed-in or returned mutable objects. In Rust, you do, and the author claims this is a problem. I say that in fact you depend on the ownership of the referenced object either way; Rust forces you to write down what it is and enforce that the callers honor it, while in Java you cannot. Arguably therefore Rust is less leaky in that hidden assumptions are documented and enforced at the API layer.
I've had lots of good experiences refactoring Rust code. It's probably more typing than other languages but the increased assurance that you didn't break the code (especially in the presence of parallelism) more than makes up for it.