← Back to context

Comment by Animats

1 day ago

Regardless of the technology the big thing Rust has that C++ does not is safety culture, and that's dominant here.

True. So many proposals have gone by over the years. Here's one of mine from 2001.[1] Bad idea. The layers of cruft in C++ have become so deep that it's a career just to understand them.

DARPA has something called the TRACTOR program, "Translate All C to Rust". It's been underway for a year, and they have a consortium of universities working on it. Not much, if anything, has come out. Disappointing.

Rust is probably too hard. I write 100% safe Rust, and there are times when I hit an ownership structure wall and have to spend several days re-planning. So far I've always succeeded without using "unsafe" or indices, but it drags down productivity.

Although object-oriented programming is out of fashion, classes with inheritance are useful. It's really hard to do something comparable in Rust. Traits are not that helpful for this.

Go is a good compromise. Safety at a minor cost in performance. Go is good enough for web back end stuff. Go has both GC and "green threads". This automates the problems that wear people down in C++ and Rust.

[1] https://www.animats.com/papers/languages/cppstrictpointers.h...

> So far I've always succeeded without using "unsafe" or indices, but it drags down productivity.

There is a common perception that Rust is less productive than competing languages, but empirical research by Google and others has found this to be wrong. Rust just shifts the effort earlier in the development phase, where the costs are often orders of magnitude lower. You may spend a few hours struggling with the borrow checker, but that saves you countless days of debugging highly non-trivial defects, especially in a larger codebase.

> Although object-oriented programming is out of fashion, classes with inheritance are useful. It's really hard to do something comparable in Rust. Traits are not that helpful for this.

FWIW, "classes with inheritance" in Rust can be very elegantly modeled with generic typestate. (Traits are used as part of this pattern, but are not the full story.) It might look clunky at first glance, but it accurately reflects the underlying semantics.

  • > Rust just shifts the effort earlier in the development phase, where the costs are often orders of magnitude lower.

    That works fantastically when you're rewriting something - you already have the idea and final product nailed down.

    It works poorly when you don't have everything nailed down and might switch a lot of stuff around, or remove stuff that isn't needed, etc.

    • > It works poorly when you don't have everything nailed down and might switch a lot of stuff around, or remove stuff that isn't needed, etc.

      I do prototype applications in Rust and it involves heavy refactoring including deletions. Those steps are the easiest ones for me and rarely gives me any headache. Part of the reason are the interfaces that you're forced to define clearly early on. Even the unrelated friction of satisfying the borrow checker gently nudge you towards that.

      The real problems are often caused by certain operations that the type system can't prove to be safe, even when they are. For example, you couldn't write async closures until recently. Such situations often require lots of thought to resolve. You may have to restructure your code or use a workaround like RC variables.

      The point is, these sorts of assumptions often don't seem to hold in practice, at least in my experience. My personal experience doesn't agree with the assertion that prototyping is hard in Rust.

    • > It works poorly when you don't have everything nailed down and might switch a lot of stuff around

      If you're prototyping code you can just do defensive .clone() calls and use Rc<> to avoid borrow checker issues. You don't need maximum efficiency, and the added boilerplate doesn't hurt that much: in fact, it helps should you want to refactor the code later.

    • I think that's actually where Rust can shine -- it's very good at refactoring, so when you move stuff around and cut things out that you don't need, the compiler tells you exactly how to put everything back together and what exactly needs to be changed. As the codebase grows, refactorability becomes crucial, because refactors are risky and can fail, causing major schedule disruptions. High code velocity achieved early on by ignoring reference lifetimes and borrows and type checking early on might feel good, but these features are a detriment later one when the project needs to start making guarantees.

  • > Rust just shifts the effort earlier in the development phase […]. You may spend a few hours struggling with the borrow checker.

    And by the time you got it figured out, the requirements change, and you’re back to struggling with the borrow checker.

    > that saves you countless days of debugging highly non-trivial defects, especially in a larger codebase.

    Seems like many projects never get to the point where the architecture toil pays off. Instead, they spend 80–90% of their time trying to find the perfect architecture, which is then brittle against change.

  • The biggest issue with Rust that I have found is that there are phase changes where making small changes to the code becomes impossible and you must completely redesign the program for it to work with the borrow checker.

  • > You may spend a few hours struggling with the borrow checker, but that saves you countless days of debugging highly non-trivial defects, especially in a larger codebase.

    How often do you had issues like that in the last 10 years of your work? Questionable if it's really cheaper for everyone. Other question, is there any really large Rust codebase out there thats older than 10 years? That had time to gather crust of tons of developers to compare with the appropriate C++ codebases? I don't think so.

> So far I've always succeeded without using "unsafe" or indices, but it drags down productivity.

I really don’t understand this perspective. The whole philosophy of Rust is one where you document why “unsafe” is safe. It is not and never has been a goal to make everything safe because that is an impossible goal to merge with high performance systems language because hardware itself is unsafe. It’s why the unsafe keyword exists. If that wasn’t the goal, unsafe wouldn’t.

  • If unsafe is not used, then no one has to determine whether the unsafe parts are actually safe.

    • Sure, but taken to an extreme you see the absurd degree you have to contort yourself. And that’s to the current version of the proof checker - some unsafe’s are even only temporary until a better prover comes by.

      You shouldn’t go out of your way to use unsafe, but between that and 2 weeks refactoring, I’ll take the unsafe and use tools like miri or ASAN to provide extra guards. Engineering is inherently about making practical choices.

  • It can be preferable to avoid unsafe when reasonable to do so. Programmers are merely human and will make a mistake at some point, and by avoiding unsafe you at least get the guarantee that the buggy behaviour is sound and (aside from race conditions) more predictable.

    • I think you misunderstand me. I’m not saying that unsafe should be the first thing you reach for. But if you can’t find an easy way to express it safely, and the only path visible is a time costly refactor, it can still be the most cost effective approach and shouldn’t be ignored.

      Even ignoring the practicality approach, there’s a reason you see it in things like crossbeam or zerocopy - not everything worthwhile expressing can even be expressed in purely safe code wether because of performance or purely because the ownership lifetime cannot be understood due to the limitations of the borrow checker even when it is indeed safe code.

TRACTOR is currently proceeding. The program is structured in phases. Each phase will present the participants with increasingly difficult challenges to translate. At the end of each phase the participants will be tested and the results of these tests will be publicly announced. The first phase of TRACTOR began in June and will run for six months.

I dislike Go's minimalism, however it fits something I have been saying for years.

Many languages that predated Java and C#, already had everything that Go offers and then some.

Modula-3, Oberon, Oberon-2, Active Oberon, Component Pascal, Eiffel.

Had Java and C#, just like those, had full support for AOT compilation, value types and the same low level programming capabilities, and many stuff that was still written during 2000-2010 in C or C++ would not have happened, and maybe C++11 would not have been as relevant as it was.

During that decade many people kept writing C or C++, because they lacked mainstream alternatives for AOT compiled languages, and not because they were into low level systems programming.

Judging by this https://vt.social/@lina/113056457969145576 rewriting any even remotely complex project to rust will require making decisions (function signatures, ownership and so on) based on information that might not be present in the C code at all, like API conventions. Translator being able to decide on all these things automatically would probably be quite close to solving the halting problem.

  • Sigh. As I point out occasionally, the halting problem very rarely comes up in practice. Combinatorial explosion, yes, but not actual undecidability.

    Understanding the implicit constraints of C/C++ functions is something where an LLM can help. Once you have the constraints recorded, they become formal constraints at the call. Historic C isn't expressive enough for even basic constraints.

    Most of the constraints mentioned are at least expressible in Rust.

> The layers of cruft in C++ have become so deep that it's a career just to understand them.

The committee have, over decades, dug themselves into a hole that they won’t be able to get out of, even if they wanted to.

C++ does have some features that I appreciate, but for every new project I begin it has to contend with Rust and OCaml, and most of the time it loses.

It will stay relevant for existing projects, but becoming the language of choice for new projects will only get harder with time.

> Go is a good compromise. Safety at a minor cost in performance. Go is good enough for web back end stuff. Go has both GC and "green threads".

Or, for even better performance, you can use Java (24+), which has both "green threads" and a more advanced and performant GC and compiler than Go.

> Go is a good compromise

Go doesn't prevent data races.. besides the borrow checker, the thing that makes Rust special is the Send and Sync traits

I think something less obvious to people is that type inheritance in C++ has several uses outside of building naive object hierarchies. Even if your model is based on composition as is typically the case these days, inheritance is a useful tool for expressing some metaprogramming mechanics and occasionally literal old style inheritance is actually the right thing to do. You don’t need it most of the time but sometimes not having it makes everything much uglier.

As of C++20 in particular, C++ has taken on a very traits-y character if you go all-in on the new language features.

The way out for C++ is probably to lean into compile-time codegen and verification within the language which is already a pretty unique capability. It dramatically reduces the lines of code a developer has to write. Defect rates closely track lines of code written regardless of the language so large improvements in compile-time expressiveness is a pretty big win.

  • Sadly we got concepts lite instead of C++0x concepts, so while better than SFINAE or tag dispatch, it is still half solution, and it won't get better because those behind it, eventually went on to Swift, and nowadays Hylo.

I think there is room for an ML with a modern toolchain story that just omits Rust's borrow checker and does something more boring. Typescript and Rust have primed a large number of developers to be open to it.

  • this is kinda the opposite of what i think people really want. they want an LLL with borrow checking without all of the abstractions and baggage you get in rust.

  • Isn't ReasonML pretty much that language already? Although the most popular language in that broader niche is probably Golang.