239 comments

[ 1.4 ms ] story [ 265 ms ] thread
This sounds like a reasonable complaint, that refactorings are too complex in large programs.

But I have a hard time to envision any other solution than "better tooling for refactoring".

True, you need to know what is and isn't possible in the borrow checking model to avoid learning it the hard way after writing the code.

There are some gotchas that you need to learn (e.g. self-referential structs won't work, or & returned from a &mut method won't be shareable).

But besides a few exceptions, it's mostly shared XOR mutable data in the shape of a tree. It's possible to build intuition around it.

This makes me further appreciate how golang's features tend to work entirely at compile time, which is also fast.

One of the other things that makes me worry about Rust is how similar it's depends look to npm projects, where there's a kitchen sink of third party (not the language's included library of code, and not the project's code) libraries pulled in for seemingly small utilities.

Go’s features work only at compile time, but are far more limited. I experience more crashes in Go than in any other compiled language because of how limited it is as a language.
Rust’s checks are also evaluated and enforced at compile time.
Dependencies are optional, and having a huge standard library also has its tradeoffs. If the standard library has a less than ideal API, it's stuck with that until a major version bump and you either:

1. End up with a third party package filling in the gaps, or

2. Another standard library API that users slowly migrate to

It's also a lot easier to release a new version of a package to fix a bug than do a bugfix release for the entire language toolchain, which is what would be needed in order to update the standard library. With Rust releasing a new minor version every six weeks, I think minimizing the chances of additional releases needed in between them is probably a good thing.
Unfortunately, in a world with increasingly more sophisticated attackers looking at supply chain attacks, having a lot of dependencies, especially ones that update regularly, is a huge security risk. For a language like Rust, which aims to be both low level and used in secure environments, I would argue that the risks far outweigh the benefits.

We'll see how this works, Rust is still young and not yet used in any hugely important projects (or at least not in hugely important parts of those projects - e.g. some Linux drivers, not the core kernel; some bits of Firefox'S rendering, not the JS engine). As it becomes more central, it's value as an attack target will increase, and people will start taking infiltrating malicious code in small but widely used dependencies.

(comment deleted)
The same could be said for all of the utilities used on Linux (that they're increasingly becoming huge targets) as seen by the recent XZ backdoor[0]. The open source model of limited funding and maintainer burnout are an inherent risk to any project. Rust is not special here.

0: https://en.wikipedia.org/wiki/XZ_Utils_backdoor

The point was to compare a fat standard library model with a model of many dedicated third party libraries. Sure, all big projects suffer from this, I'm not trying to single Rust out. But both Rust and those other projects have to learn from this that the model is bad.

Having a large and good standard library, supplied by a single trustworthy foundation, with dedicated employees that check incoming PRs, is going to become more and more important in the following years.

I think it's the natural state of affairs for a "folk standard library" to emerge. I don't think pydantic or serde should be part of their standard libraries. But I will use them in most projects. In ten years, the "folk stdlib" will probably be a different set of packages (perhaps a superset, perhaps not). Don't push the river; if it's natural, manage it rather than fighting it.

Trying to anticipate all or even most use cases in the standard library is a fool's errand (unless we're talking about a DSL, of course). There are too many and they are too dynamic to be captured in the necessarily conservative release process of a language implementation. Languages should focus on being powerful and flexible enough to be adapted to a wide variety of use cases, and let the community of package maintainers handle the implementation. Think of this as a special case of the Unix philosophy; languages should do one thing very well, not a million things unevenly.

I bet most people here don't believe a command economy could ever work in a market for goods and services. Why should it work in a marketplace of ideas?

And new cars tailored for each consumer’s use case will emerge in ten years, too—that doesn’t make it any less awful to live in areas that lack good public transportation.
I'm not sure I understand the metaphor? Let me know if I'm off base.

If the suggestion is that putting things in the standard library makes them better, I disagree. My experience with Python for instance is that a "batteries included" strategy results in some phenomenal packages and some borderline abandoned packages that are actively dangerous to use.

To riff on your metaphor, the federal government designs the arterial highways, but the state, country, and city/town officials design the minutia of the traffic system. If the federal government had to approve spending on replacing some street signs or plowing snow, we would have a terribly impoverished transportation system.

The suggestion is that, while it is possible to overdo a stdlib, it is also possible to underdo it.

For two examples: plenty of languages leave auto-formatting and testing to the community, functionality which rust is better for having standardized.

I agree with this. However memorizing the borrow checker rules has led me to architect code “more correctly” upfront and consider things like ownership that I was otherwise pretty lax about ahead of time before. I think this has made me more thoughtful even in other languages which I think has been a win for me.

That said, the tedious refactors are a real pain. I think we all hoped that rustc would be smarter by now. It has gotten better but it isn’t there yet.

Same for me. I think I write better code in other languages now.
what a miss.

i’d consider myself a day-to-day c++ engineer. well, because i am. i like lots of things from rust. there’s a few things i don’t. c++ has a lot to learn from rust, if it is to continue to exist.

but really.. isn’t this the point of the language? you need to understand the borrow checker because.. that’s why it’s here?

maybe i’m missing something.

Rust was a pain in the ass until I stopped trying to write C code in it and started writing idiomatic Rust. I don’t know the author of this blog, but he mentions extensive C++ experience which makes me wonder if he’s trying to write C++ in Rust.

Maybe not! Maybe it’s truly just Rust being stubborn and difficult. However, it’s such an easy trap to fall into that I’ve gotta think it’s at least possible.

Are there examples one can learn from about idiomatic rust? I would appreciate either books or projects to learn from.
https://doc.rust-lang.org/book/ is great. I’d been writing Rust for months before I started reading it and still began learning new things from the start. Oh, that’s why it does this!

Edit: Oh! And use “cargo clippy” regularly. It makes excellent recommendations for how to make your code more idiomatic, with links to docs explaining why it’s nicer that way.

Rust, like ocaml, is best when used purely functionally until you run into something that isn't performant unless its imperative. But unlike ocaml or haskell there is a safe imperative middle ground before going all the way to unsafe. People who write modern C++ with value semantics etc. seem to have a lot less trouble than people coming from Java.
I mean, I don’t write it that way, but if it works for you. I wouldn’t say you have to write it that way so I wouldn’t want to put anyone off.

Thinking about your answer a bit more, one of the paradigms of Rust is “there shall be many immutable references or just one mutable reference” and so I can see that functional programming would naturally lead to that. But it’s a paradigm that works with the underlying principles rather than the true nature of the language, IMHO.

I do it by thinking about different domains of object graphs, and how data moves between them, for example.

It's difficult to really use Rust purely functionally given that it removed pure functions from its type system, and that has a limited stack size.
Most Rust code is not purely functional in my experience. It's quite similar in style to C++ except:

1. No class hierarchies and inheritance.

2. The borrow checker forces a tree structured ownership style. You don't get spaghetti ownership. This is generally great because that coding style leads to fewer bugs. But sometimes it is annoying and you have to use indices rather than pointers as references.

Generally I have the easiest time when I declare my state in the outermost scope possible, and then pass it into functions that need to operate on it. If I'm using an actual pointer, rather than a mutable reference that came in as an argument, something weird is happening! Usually that's the interface with some external library.

Rust in particular is *really* obnoxiously bad at OOP patterns, and I think my lesson at this point is that this is because it is hard to do OOP safely, at least in a way that jives with its borrow checker. Something like functional core, imperative shell seems to be a much nicer flow for the thing in general.

Anyway, I've just got the one major Rust project (an NES emulator) so I'd say I'm pretty early in my Rust journey. For me personally, the good points (delightful match, powerful enum) outweigh the bad (occasional borrow checker weirdness, frustrating lifetimes) but I think it depends a lot on what you're trying to do with it.

You can achieve some level of OO design by using traits (the generic kind, not the dyn kind), but I think the functional style and inline testing gives you a ton of nice properties for free!

Rust also pushes you to refactor in a way that really pulls out the core of your problem; the refactoring is just you understanding the problem at a deeper level (in my experience)

I learned Rust before learning C properly.

Oh boy. I see bugs everywhere in C and why the borrow checker exists. It really forces you to understand what happens under the hood.

The most issues in Rust are indeed related the expressions - you don't know how to describe some scenario for compiler well-enough, in order to prove that this is actually possible - and then your program won't compile.

In C, you talk more to the computer with the language syntax, whereas in Rust you talk to the compiler.

> Oh boy. I see bugs everywhere in C and why the borrow checker exists.

Any examples that you could provide? I have been dealing with C/C++ for close to 30 years. Number of times I have shot myself with undefined/unspecified behavior is less than 5.

The borrow checker isn't just about UB, it is mostly about memory safety.

I'm sure you've seen plenty of use-after-frees/use-after-move/dangling pointer type things or null pointer derefs, or data races, etc etc. These are largely impossible to do in safe rust.

The number of times you shot yourself in the foot that you know about. Some of those bullets just haven't landed yet. C and C++ give you very interesting foot-guns: sometimes they go off even when you don't touch them (compiler upgrade, dependencies changing, building on a new architecture, ...)
In 30+ years of experience in C, you haven't used a free()d variable or written past the end of a buffer more than 5 times? If that's true, then you have more care and attention than 99.99% of all C experts.
I should have been clear.

Of course, I have done such mistakes, but they were caught early in the dev. process. I am talking about bugs that were caught in production due to misunderstanding of C compilers on 16/32 bit processors.

I also avoid idioms like *p instead write p[i] whereever possible.

Clearly you must be superhuman then, something as simple as forgetting a null pointer check is bound to hit you every now and then.
Of course, I do, but they are caught early in the dev. process. Not in production though.
I would contend that’s an unusually sophisticated dev process not used by most.
> In C, you talk more to the computer with the language syntax, whereas in Rust you talk to the compiler.

The C compiler pretends to be the computer. But UB is still there, as a compiler-only thing that has no representation at all on the computer.

> In C, you talk more to the computer with the language syntax, whereas in Rust you talk to the compiler.

C is like a fast motorcycle, rust is a car with driver-assistance system.

> Rust was a pain in the ass until I stopped trying to write C code in it and started writing idiomatic Rust.

This is the #1 problem I see with people trying to learn a new language (not just Rust).

I’ve watched enough people try to adopt different languages at companies over the years that I now lean pessimistic by default about people adopting new languages. Many times it’s not that they can’t learn the new language, it’s that they really like doing things the way they learned in another language and they don’t want to give up those patterns.

Some people like working in a certain language and there’s nothing wrong with that. The problems come when they try to learn a new language without giving up the old ways.

Like you, I’m getting similar vibes from the article. The author wants to write Rust, but the entire premise of the article is about not wanting to learn the rules of Rust.

> This is the #1 problem I see with people trying to learn a new language (not just Rust).

Definitely! I've also noticed people will learn a group of similar languages, like Java, C#/.Net, then Kotlin as the most distant relation. Now, they think they know many languages, but they mainly have the same core idea. So when they try something new like Haskell or Swift or Rust, they think it's doing something different from the "norm" in a really irritating way.

Trying to convince developers from "classical" OO that not everything needs to be a class in JavaScript has been a major thorn in my side for years. Your little procedure with no state? That can just be an exported function.
> The problems come when they try to learn a new language without giving up the old ways

In Python, I frequently see the same problem from the other side. Instead of C/C++ programmers learning Rust and "not wanting to learn the rules of Rust", it's Java/C# programmers learning Python and not wanting to unlearn the rules of Java/C#. They write three times as much code as they need to - introducing full class hierarchies where a few duck-typed functions would do.

it’s not just languages.

i had similar problem with tensorflow for like a year or so until it “clicked” i was trying to make the framework do thing the way i wanted, instead of doing things the way the framework wanted.

seeing that in myself was a real eye opener.

now i see it in others too and it’s frustrating because, ultimately, no-one can tell them they’re doing it. they have to find out for themselves for it to make a difference for how they act.

Would you be able to give an example of what you mean? What it would look like trying to write C code in Rust and then the idiomatic Rust way?
It is interesting that the borrow checker doesn't run until after typechecking succeeds. As far as I'm aware, rust-analyzer has its own builtin logic for doing typechecking, but it delegates to rustc for borrow checking. I wonder whether this is just a temporary situation due to lack of engineering resources to implement borrow checking in rust-analyzer; personally I doubt that, especially since gccrs is incorporating components of rustc wholesale and so I'd be a bit surprised if rust-analyzer moves in the opposite direction. In theory it seems possible to support borrow checking in the IDE for ill-typed programs, but having borrow checking as a separate analysis pass depending on successful typechecking is just such a nice abstraction boundary to have for maintaining the toolchain.
borrow checking relies on types to be able to check; types are what carry borrows after all.
This is true but misleading: analogously, typechecking depends on parsing, but IDEs typically make a best effort to typecheck syntactically ill-formed programs.
rustc does its best to recover, continue and provide diagnostics from later stages. But at the same time it is better to provide a single early error and mark the entire node as recovered to avoid further errors at the cost of requiring more cycles of back and forth, over the alternative of tons of useless knock down errors that drown the underlying cause of the problem.

We are always on the lookout for improving in this area. Having examples of cases where we conceivably should have done better but didn't is useful. As mentioned already, the complexity here is that doing the right thing for the user requires architecting multiple separate stages of the compiler to talk to each other in way more complex ways than originally intended.

Yes, and people should stop using Rust for projects that do not require a systems programming language.

Know what tool to pick.

I used it for an API server and it was pleasantly fun. “Systems” is a broad brush.
It's a high level language with algebraic data types and fantastic type checking. I'll use it where ever I darn well please.
Sometimes I write Rust with Arc and Rc sprinkled everywhere because I just need a dialect of OCaml.
The only thing that's really missing is deref patterns so that you can pattern match on an Arc<T> with a T pattern.
I mean having the following work:

  match Arc::new("hello") {
      "hi" => {}
      "hello" => {}
      _ => {}
  }
It is an extension of match ergonomics, called deref patterns. There's some experimental code to make it work for `String` to `&str` (`if let "hi" = String::from("hi") {}`), but it is not anywhere close to finished. The final version will likely have something like a `trait DerefPure: Deref {}` signaling trait. There is disagreement on whether giving users the possibility to execute non-idempotent, non-cheap behavior on `Deref` and allow them to `impl DerefPure` for those types would be a big enough foot-gun to restrict this only to std or not.
What's the difference and advantage, other than less typing, of this over:

     match *Arc::new("hello").as_ref() {
      "hi" => {}
      "hello" => {}
      _ => {}
(Sorry, Rust newbie here)
Patterns can be arbitrarily complex. If you have

  struct Foo {
      bar: Arc<String>,
  }
Then it's the difference between today's

  if let Foo { bar } = val {
      if &*bar == "hi" {
      }
  }
And the potential future

  if let Foo { bar: "hi" } = val {}
The more complex the pattern, the bigger the conciseness win and levels of nesting you can remove. This comes up more often for me with `Box`, in the context of AST nodes, but because I use nightly I can use `box` patterns, which is the same feature but only for `Box` and that will never be stabilized.
Sure. But pick based on concrete knowledge of the tool rather than broad and abstract categories. Tools are routinely discovered to be more broadly applicable than their initially intended use case, or even to be poor fit for that use case. The map is not the terrain.

And in any case, ecosystem usually trumps other concerns. Deep learning is a task you'd think a system language would be ideal for. Yet Python is a probably a better choice than Rust. The best reason to avoid Rust for a new project is probably that the market for Rust developers is insufficiently liquid (though presumably that won't be true for much longer, if it's still true at all).

People conflate difficulty of Rust borrow checker with the inherent difficulty in systems programming. However, this is not true. Both C and C++ are primarily systems programming languages. However, that never dissuaded anyone from using them as general purpose programming languages. The same should apply to Rust as well.

Meanwhile, Rust is my programming language and I choose it unless there is a good reason to choose another language. I never really struggled with the borrow checker. I think a lot of beginners approach the BC wrongly. Trying to memorize the rule is definitely the wrong way.

An often overlooked solution to this problem is to avoid using Rust, or to only use it for performance critical code. Writing a large application in Rust sounds hellish, it seems like it would be much nicer to only use it in sections of the hotpath where it is absolutely necessary.
That applies only if you're struggling with Rust. It's as good as any other general purpose programming language once you're out of the fight-the-borrow-checker stage. Structuring or refactoring large applications in Rust is nowhere as tedious as many project it. There are many zero-cost abstractions and other features that even makes it very pleasant.

My first preference for making simple utilities is as a shell script. The immediately next one is actually Rust.

Manually managing memory complicates the program, and makes it harder to change. Every interface written is contaminated with the implementation details of how it manages memory. This has a large cost in terms of development time. As much as you might want to imagine Rust has made every other programming language obsolete, that just isn't true.
Your assertion doesn't match my experience. Besides if that were true, nobody would be using C or C++ for writing general purpose (non-system) applications. C and C++ require the same system knowledge that Rust developers use to satisfy the borrow checker. Even worse, Rust borrow checker will remind you of those rules. C and C++ will just allow you to proceed and crash. C and C++ memory management is even more manually involved than Rust's. Yet people do write normal applications in C and C++.

The only explanation I can think of for the dislike towards Rust's compile time checks is that some people don't entirely understand these rules when they use C and C++. It's possible to resolve simple memory safety issues in C/C++ without in-depth knowledge of hardware semantics. But a complicate bug will easily stump you at runtime (personal experience).

You seem to have forgotten that garbage collected languages exist, and are much preferable to Rust in many circumstances.
You're diverting from the context of this thread. I'm asking why a developer who is comfortable with Rust borrow checker choose any other language? How is a GC language preferable in any way to Rust for such a person?

In a broader sense, I keep seeing some people asking everyone else to avoid Rust based on an exaggerated account of the struggle with the borrow checker. There is actually a way to get comfortable with the BC. Perhaps beginners should be introduced to those ideas rather than such negative takes.

> I'm asking why a developer who is comfortable with Rust borrow checker choose any other language?

Because there are other languages which don't require you to hold to a strict set of rules to maintain memory safety, as they automate memory reclamation at runtime.

> There is actually a way to get comfortable with the BC. Perhaps beginners should be introduced to those ideas rather than such negative takes.

Regardless of how comfortable you are with it, it imposes a necessary overhead, either in terms of architectural constraints or the runtime cost of reference counting which is greater than that of garbage collection.

> I keep seeing some people asking everyone else to avoid Rust based on an exaggerated account of the struggle with the borrow checker.

The borrow checker is irrelevant here. As you have mentioned, a person coding is C will have to keep to the same constraints which Rust statically enforces. There is clear reason to use a language such as Java over C. Not having to manually manage memory simplifies code and makes refactoring easier. This is true regardless of if your memory restrictions are checked or unchecked. Imagine the simplest example: a data structure containing a string. In Java, you write a class and put a string in it. The data for the string is shared and a simple copy of the pointer will suffice in the constructor. In languages with manual memory management, you have three options: the struct can own the string (or reference count it), in which case it will have to be cloned and performance will be worse than garbage collection; the struct can borrow the string, in which case you'll have to track the string's lifetime throughout the program which could cause issues later; the structure can be generic over owned and borrowed string types, in which case code will be more complicated and generic parameters will proliferate. It should be immediately apparent that the latter requires more architectural consideration than the former, and hence is a greater burden to programmers. Several of the options in the Rust case will also lead to potentially complicated refactoring later, which adds even further cost to development.

TL;DR managing memory manually takes more effort. That's why it's called MANUAL memory management, because it's not automated.

My opinion so far is that people want to write Java using Rust, or they want to write C++ using Rust, and they have a hard time. You can’t really just go write a program architected the way you do in those other languages. It’s not “C++ with a borrow checker”.

So I’d say it’s “worse” than “you have to memorize the borrow checker”. Its “you have to learn how to write programs in Rust”.

The borrow checker exists to force you to learn, rather than to let you skip learning. To make an analogy, I think it would be weird if I complained that I had to "memorize the rules" of the type checker rather than learning how to use types as intended.
Fair enough, but the problem in this analogy is that this learning isn't always useful or productive in any way. This is more like doing arithmetic in a sort of maths notation where every result must be in base 12 and everything else must be in base 16. Sure, you can memorise the rules and the conversions but you aren't doing much useful with your life at that point.

Obviously, the borrow checker has uses in preventing a certain class of bugs, but it also destroys development velocity. Sometimes it's a good tradeoff (safety-critical systems, embedded, backends, etc.) and sometimes it's a square peg in a round hole (startups and gamedev where fast iteration is essential)

(comment deleted)
I think it destroys productivity if and only if you don’t roll with it and do things the Rusty way. If you write code with its idioms, it can be a huge productivity boost. Specifically, I can concentrate on fixing logic errors in my code instead of resource bookkeeping. When I refactor something, I know I didn’t accidentally forget to move alloc/free to the appropriate places for the new code: if my changes broke something, it’ll tell me.
Rust shouldn’t “destroy development velocity” once you’ve grasped the core concepts. There is some overhead to being explicit about how things are shared and kept, but that overhead diminishes with time as you internalize the rules.
Not if you're iterating and have to make fundamental changes. Just like certain advanced type systems, encoding too much at compile time means you have to change a lot of code in unnecessary mechanical ways when the design constraints change, or when you discover them.

This is not a bad thing by the way, it's an extremely plausible design chocie, and is one that Rust made very clearly: rejecting not-entirely-correct programs is more important than running the parts that do work. Languages that want to optimize for prototyping will make the opposite choice, and that's fine too.

If the borrow checker only errd on code with bugs you could call it learning. Or if it was only possible to express correct programs in the Rust language. But such a thing isn't possible in general so we accept the weaker condition, accepting a subset of all valid code that can be proven correct. The usability of the language goes with how big that subset is, and the OP is expressing frustration at the size of Rust's.

Rust isn't alone in this, languages with type hints are currently going through the same thing where the type-checker can't express certain types of valid programs and have to be expanded.

Besides, if you still want to skip learning there are escape hatches like Rc<RefCell> but these hint pretty strongly (e.g. clones everywhere) that something might be wrong somewhere.
> Rust is not the answer, it is simply a step towards the answer.

True that.

On one hand, it's amazing. On the other hand, the nagging feeling that we still have work to go in programming language design has not gone away.

There is no answer. There are only improvements.
Two examples of things I want, built-in on day one, in some future language:

Structured concurrency. Don't provide "legacy" mechanisms and "opt in" for structure, just bite the bullet. Like the first language which told people no, we don't "go-to" other functions, that's not happening in my language, that was structured program flow I want structured concurrency, it's a thing but it's not yet popular enough to do that as the only provided concurrency, it should be.

Smart arithmetic. Your computer has Floating Point math. FP math is fast, but, it's hard for humans to think about exactly where they lose precision and performance while using it. I should be able to write the real mathematics I want, specify the precision I need and possibly the performance trades I'm comfortable with, and the compiler not me the programmer, figures out how to use FP math to calculate my mathematics with acceptable precision or tells me that I made demands it couldn't meet or which are nonsensical.

On the other hand, two things I really liked to see when I learned Rust:

&[T]: The slice reference type, a fat pointer which specifies where zero or more contiguous Ts are, and, how many of them. This is the Right Thing™ and it's right there in the core language design, which means you don't need to go back and retro-fit it.

String: The simplest possible way to build the growable text type, as a growable array of bytes but with the strict requirement that the array's content is always UTF-8 text. Is the "Small String Optimization" a cool trick? Yeah, but it need not live in this core vocabulary type. How about Copy-on-write ? Ditto. What about other text encodings? Transcode at the edges if you need that.

I sincerely believe that it is nearly impossible to have an objective and constructive conversation about the merits of programming languages, because the language of choice becomes part of people's worldview.

So it's like discussing politics or religion. People think that they have objective views, but they can't overcome their beliefs. That's just how beliefs work. They almost never change.

Also, beliefs are tied to groups. Humans automatically adopt the beliefs of their group, at least to some degree. Or they learn to shut up about their disagreements.

This is a thread for Rust critics and Rust advocates. Try to seriously sell F# or some other ML-like language in here and you are going to end up annoying both the C++ people and the Rust people.

The world will be a better place when the AIs finally take over. If we survive.

In my opinion, it seems like you may be taking random internet discussions a bit too seriously; I don't actually expect too much meaningful programming language discourse to occur in Hacker News comments. I think the reason why I keep coming here is in part because it's one of the rare public forums where occasionally some truly interesting discussions really do happen, but don't forget Sturgeon's Law. For better or worse, public and open forums are rarely productive places to have discussions, and a lot of the real innovations certainly seem to happen behind closed doors. (Personally I greatly prefer public forums for discussion, and even would prefer anonymity if it were feasible, but I take what I can get.)

What does that say about participating here? Well, for me, sometimes when I write a comment that I feel is constructive, reasonable, and honest, it goes gray anyways, and it's easy to chalk it up to people just irrationally downvoting it because they don't like my opinion. It's also pretty easy to do this, I just need to be cynical about Apple or optimistic about the Go programming language, or something similar, and there's some percentage chance it will go negative depending on presumably who sees it first. It's not going to stop me from doing so, and ultimately it's pretty inconsequential, as I'm just some guy and my opinions are not really that important anyways.

Somehow, even though I have all of this internalized, I can't help but go 30 nested replies deep into threads debating about something senseless and unimportant, but it almost feels like it wouldn't be the Internet without debates like that. XKCD 386.

I have memorised the UB rules for C. Or rather, more accurately, I have memorised the subset of UB rules I need to memorise to be productive in the language and am very strict in sticking to only writing code which I know is well defined (and know my way around the C standard at a level where any obscure code I sometimes need to write can be verified to be well defined without too much hassle). I think Rust may be difficult But, if I forget something, or make a mistake, I'm screwed. Yes there's ubsan, there's tests, but ubsan and tests aren't guaranteed to work when ub is involved.

This is why I call C a minefield.

On that note, C++ has such an explosion of UB that I don't generally believe anyone who claims to know C++ because it seems to me to be almost infeasible to both learn all the rules, or at least the subset required to be productive, and then to write/modify code without getting lost.

With rust, the amount of rules I need to learn to understand rust's borrow checker is about the same or even less. And if I forget the rules, the borrow checker is there to back me up.

I still think that unless you need the performance, you should use a higher level language which hides this from you. It's genuinely easier to think about.

That being said, writing correct rust which is going to a: work as I intended and b: not have UB is much less mentally taxing, even when I have to reach for unsafe.

If you find it more taxing than writing C or C++ it's probably either because you haven't internalised the rules of the borrow checker, or because your C or C++ are riddled with various kinds of serious issues.

> This is why I call C a minefield.

Computing is a series of "minefields." At least you get a map of this particular one.

I'm far more confronted by public facing APIs that involve user authentication than I am of any particular documented set of language facts.

The ISO document for C has an appendix which lists all the known categories of Undefined Behaviour. It's not exactly a small list, but it's something you could memorize if you wanted to, like the list of all US interstates, where they start and where they end.

There has been a proposal to attempt this for C++ but IMO the progress on making such an appendix is slower than the rate of the change for the language, making it a never ending task. It was also expanded by the fact that on top of Undefined Behaviour C++ also explicitly has IFNDR, programs which it declares to be Ill-formed (ie they are not C++) but No Diagnostic is required (ie your compiler doesn't know that it's not C++). This is much worse than UB.

This only makes sense if one wants to write a Phd on C++ UB and needs the exhaustive list.

For the rest of us, there’s cppreference, UBsan and quite a few books on writing correct C++ code. Of course, these will still not suffice to write 100% memory safe code, which is a pretty arbitrary goal that just happens to match what Rust offers and is pushed a lot by Rust advocates.

It’s a nice goal, but not everybody works on software that’s attacked all day every day.

Also, memory safety isn't the only "bug" - I'd even argue that the majority of "memory" issues in unsafe languages like C are actually the result of a logic error or mismatch of interface expectations, and a memory error is often the "first noticed failure". In the trivial example strcpy() examples people love to use, unexpectedly truncating a string often means the program has "failed" in it's intended task just as much as a segfault or other memory corruption.

I'm extremely positive on highlighting as many of these problems before it gets to the user's hands, even more so if it's as early as a compile step as in the borrow checker, but lets not delude ourselves that they are the only possible issue software has. Or that in many languages it's a tooling issue (or culture issue accepting that tooling...) rather than a fundamental language difference.

On a side node, with the prevalence of things WASM I feel some people are just redefining what "memory safety" is. Defining a block of memory and using offsets within that is just reinventing pointers, the runtime ensuring that any offsets are within that block just mirroring the MMU and process isolation. We should really be looking at why that isn't well used rather than just reimplementing a new version on top for "security", as if those reasons aren't really "technical" (IE poor isolation between "Trusted" and "Untrusted" data processing in separate processes due to it being "Easier") we need to ensure we don't just do the same things again, and if they are technical we can fix them.

That's the appendix containing documented UB. The standard also explicitly states that any behaviour not explicitly defined by the standard is undefined meaning that there are things which aren't in that list. And I can confirm, there are things which you can do in C which are UB but which are not on that list.
Embedded. Your UB is my opportunity.
Really? So far it seems like most of the UBs in C are caused either by:

1. Masochism

2. Underspecification, in a vain attempt to make a language that can theoretically be used on PDP computers.

You’re missing #3, which accounts for an absolutely enormous amount of loss:

3. The fact that an inappropriate write through a pointer results in behavior that is so undefined that it can lead to remote code execution and hence do literally anything.

No amount of additional specification can fix #3, and masochism cannot explain it.

One could mitigate #3 to some extent with techniques like control flow integrity or running in a strongly sandboxed environment.

That's not missing, I think they left it out of the "most" criticism on purpose. A dangling pointer is one of the few really good cases for UB. (Though good arguments can be made to give the compiler less leeway in that situation.)
> The fact that an inappropriate write through a pointer results in behavior that is so undefined that it can lead to remote code execution

This is a strange way to look at it. You'd get remote code execution only if the result of writing through the pointer was exactly what you'd expect: that the value you tried to write was copied into the memory indexed by the pointer.

There's nothing really you can do with out-of-bounds write in C except say that it can do "anything". This UB is unavoidable.

I'm talking more about the nonsense like "c++ + ++c". There's no reason but masochism to keep it undefined. Just pick one unambiguous option and codify it.

An example of #2 is stuff like signed overflow. There are only so many ways to handle it: wraparound, saturate, error out. So C should just document them and provide a way to detect which behavior is active (like it does with endianness).

It's someone disingenuous to purposefully ignore what is the most common kind of UB in C. It's also ultimately not a very useful dichotomy, especially because it misunderstands why behavior ends up being undefined. For example:

> I'm talking more about the nonsense like "c++ + ++c". There's no reason but masochism to keep it undefined. Just pick one unambiguous option and codify it.

It's because there's an underlying variance in what the compilers (and the hardware [1]) translated for expressions like that, and codifying any option would have broken several of them, which was anathema in the days of ANSI C standardization. (It's still pretty frowned upon, but "get one person to change behavior so that everybody gets a consistent standard" is something the committees are more willing to countenance nowadays).

> An example of #2 is stuff like signed overflow. There are only so many ways to handle it: wraparound, saturate, error out.

Funnily enough, none of the ways you mention turn out to be the way it's actually implemented in the compiler nowadays.

As for why UB actually exists, there are several reasons. Sometimes, it's essential because the underlying behavior is impossible to rationally specify (e.g., errant pointer dereferences, traps). Sometimes, it's because you have optimization hints where you don't want to constrain violation of those hints (e.g., restrict, noreturn). Sometimes, it's erroneous behavior that's hard to consistently diagnose (e.g., signed overflow). Sometimes, it's for explicit implementation-defined behavior, but for various reasons, the standard authors didn't think it could be implemented as unspecified or implementation-defined behavior.

[1] Remember, this is the days of CISC, and not the x86 only-very-barely-not-RISC kind of CISC, the heady days of CISC where things like "*p++ = --q" is a single instruction.

Not really.

In embedded environments you're constrained by toolchain and platform but it's still a bad idea to rely on any behaviour which your compiler doesn't provide a definition for (which might be more behaviour than what your standard provides a definition for) because changes to the version of the compiler or even changes to surrounding code can trigger issues caused by reliance on UB.

It's not actually that hard to write embedded code which does not invoke UB outside of register access and even there it's possible to limit yourself to invoking behaviours which the combination of hardware + compiler does provide documented behaviour for.

(source: I've written embedded code which did not knowingly/intentionally invoke UB outside of register access and in those cases the implementation did define behaviour.)

> I still think that unless you need the performance, you should use a higher level language which hides this from you.

Exporting and consuming the full c abi with very little effort is also another huge thing in rust's favor. Languages have opted heavily for supporting calling into the c abi and being hosted by the c abi, so naturally support for rust on the same terms comes for free. There's even rust in linux now.

I think you’re missing the author’s point, but OTOH he undermined it himself by stating that learning the rules helps: because Rust requires that the ownership and relationships are encoded in the type system, it requires significant design changes when those relationships change.

Learning the rules only partly mitigates this, because sometimes one does exploratory programming and isn’t sure what the final types are or they just want to change something.

Rust thrives on over-specification which calcifies the APIs.

Anyway, just as the author’s allegedly holding Rust wrong, one could say that you’re holding C++ wrong - the right approach is to learn how to write correct code and then the exceptions. Also accept and be at peace with the fact that your code will have some bugs. I don’t know why the average Rust developer is so obsessed with getting things perfect and no less with memory safety when the overall software quality is the way it is. I mean if someone’s researching the topic or works on Rust, sure, be the Stallman of memory correctness.

I think unless your code is guaranteed to never interact with any untrusted input it is nowadays an increasingly unacceptable compromise to just accept that your program might have serious flaws which can lead to remote code execution or worse.

Moreover, it becomes increasingly unpleasant and unworkable to deal with code which progressively gets more and more unreliable.

It's expected that if the complexity of a program grows, the state space that the program can occupy grows with it. But with UB you can run into by accident that state space seems to grow exponentially in comparison to a language like Rust.

If you are required to write code at that low level, I would not use anything other than something like rust.

If you are not required to write code at that level. There are many languages with much less uncertainty than C++ which are much more productive than either C++ or rust.

> I think unless your code is guaranteed to never interact with any untrusted input it is nowadays an increasingly unacceptable compromise to just accept that your program might have serious flaws which can lead to remote code execution or worse.

I think that's too strong a statement, because it applies to in-development programs. I agree with you if you're talking about released programs, but there can be benefit in leaving open the possibility of detectable flaws, serious or otherwise, while your code is still in development.

It's analogous to only compiling and running in debug mode throughout your development, and then switching to release mode for the final binary. The binary is suboptimal throughout your development process; it's too slow. But as long as the `--release` flag doesn't require any code changes, it's still a better idea than developing entirely in release mode.

Similarly, the binary could be suboptimal from a correctness standpoint, as long as removing the `--devel` flag only works when the compiler is fully happy. `--devel` could turn some borrow checking failures into warnings and still give you a runnable binary. Or it could allow leaving types underspecified in interfaces, and do an unsound type inference. Best case, it could even do runtime checks and/or coercions to establish the assumptions that the callee was compiled with.

Whether it would be worth the complexity is an open question, but it seems reasonably clear that Rust has a problem with brittleness to development-time change.

If you develop C or C++ haphazardly in such a way that you leave a bunch of UB on the table during development then there's little to no chance that you'll have actually erased all presence of it by the end of development.

There currently exist no tools which with complete reliability point out all UB in your program. If any part of your program can have UB and you didn't write it with the explicit intention of not having UB in it at any point then you're going to be left with a tough situation to deal with.

I've read a lot of C in my time and there's codebases which I read and find easy and quick to review because they stick to the rules and only bend them sparingly and then there's codebases which are a pain to evaluate even the most basic parts for errors and UB.

There's no such thing as "detectable UB", there's only UB which your tools have luckily managed to detect.

Leave the UB to the people who can't avoid it, stick to safe languages when you can.

After reading the article, it’s clear the author approves of the fact Rust has these rules (and prefers it over C++). They’re highlighting the natural challenges that brings so future iterations or competitors can see what needs to be improved.
> I have memorised the UB rules for C.

Why? What's wrong with using one of the many static code analysis tool to tell you about them if/when they appear?

Those tools can't reliably identify undefined behaviour.
> Those tools can't reliably identify undefined behaviour.

I'm sorry, can you explain what leads you to believe your hypothetical scenario is an argument rejecting the use of static code analysis tools?

I mean, I'm stating the fact that there are many many tools out there that can pick up these problems. This is a known fact. You're saying that hypothetically perhaps they might not catch each and every single hypothetical case. So what?

They're a good idea, but not a substitute for knowing the rules. And they don't just miss theoretical cases, they miss problems in practice even when used rigourously.
> They're a good idea, but not a substitute for knowing the rules.

It's a good thing no one made that claim, then.

The whole point is that were seeing people in this thread making all sort of wild claims on how it's virtually impossible to catch these errors in C++ even though back in reality there are a myriad of static analysis and memory checker tools that do just that.

Your average developer also knows how to type in a space character but still it's a good idea to onboard linters and automatic code formatters.

You made the claim

> Why? What's wrong with using one of the many static code analysis tool to tell you about them if/when they appear?

You clearly pose static analysers as an alternative to understanding UB. You still need to understand how things work.

It's not impossible to catch those errors in C and C++. In fact, every time you run a new tool against a large C or C++ codebase you will find new ones. What none of these tools do is catch all the issues, as demonstrated by the fact that people keep finding new ones.
Rust can be hard to get right because of the borrow checker. I had a similar situation happen to me where I went about refactoring the code to make borrow checker happy ... until the last bit when things stopped compiling and I realized my approach was completely wrong (in the rust world, I had a self-reference in the structs)

Having said this, the benefits of borrow checker out weight the shortcomings. I can feel myself writing better code in other languages (I tend to think about the layout and the mutability and lifetimes upfront more now)

My rust code now is very functional, which seems to work best with lifetimes.

I would love to know more about the authors pain, I do hope rustc gets better at lifetime compilation errors cause some of them can be very very gnarly.

> I do hope rustc gets better at lifetime compilation errors cause some of them can be very very gnarly.

When this happens, file tickets! We do our best to improve diagnostics over time, but the best improvements have been reactive, by fixing a case that we never encountered but our users did.

will keep that in mind going forward! The most recent ones which I have been hitting are around "higher-ranked lifetime error"

I know my way around this now, which is to literally binary search over the timeline of my edits (commenting out code and then reintroducing it) to see what causes the compiler to trip over (there might be better ways to debug this, and I am all ears)

Most of the times this error is several layers deep in my application so even tho I want to ticket it up, not being able to create a minimal repo for anyone to iterate against feels like a bit of wasted energy on all sides, do let me know if I should change this way of thinking and I can promise myself to start being more proactive.

If it's public code, a link to a branch with the issue can still be useful. Looking at the compiler internals you can get a better sense on how to minimize the issue. That being said, not having a minimised repro lowers the chance of it getting addressed quickly.

Even if you have already figured out how to deal with it, your future colleagues might not, and by improving the diagnostic you would also be getting that time manually commenting code back.

> Rust can be hard to get right because of the borrow checker.

In the same vein: «C/C++ can be hard to get right because of the valgrind.» ;-)

I don't really agree with this. I'd phrase it more as, you have to learn to really understand what the borrow checker is trying to do and how it makes you architect your programs and consider that ahead of time. Once you understand that, you'll rarely have problems with the borrow checker. It does preclude significant chunks of styles and data structures often used in other languages though.
> it makes you architect your programs and consider that ahead of time

This only works for projects which do not involve any R&D, but have a complete and well written functional specification written in advance. Also for projects which do a complete re-implementation of some pre-existing software.

For greenfield projects which require substantial amount of R&D, it’s impossible to architect programs ahead of time. At the start of the development, people only have a wishlist. Architecture comes later, after several prototypes implemented and evaluated, and people have some general understanding what does and doesn’t work, and what specifically needs to be done.

Rust implies that upfront architecture costs even for prototypes.

Agreed. This is probably the reason why rewriting in Rust "works" -- because the architecture has been previously worked out.
I don't really agree with that interpretation. In my opinion and experience, it does not restrict architectural choices to the extent that it makes it difficult to develop greenfield projects. It's more that it rules out a relatively small subset of architectural choices which are arguably a bad idea anyways, as they do infact have flaws that may not be obvious at first but will lead to a lot of pain if the project grows above a certain size.
> rules out a relatively small subset of architectural choices which are arguably a bad idea anyways

Just because an algorithm is not representable in safe rust doesn’t mean it’s a bad idea. See my other comment in this topic https://news.ycombinator.com/item?id=42164582 You’ll find similar parallel algorithms in all high-performance BLAS libraries. On my day job I sometimes do similar things in C++, using OpenMP or other thread pool for parallelism.

That I can agree with. I think Rust strikes a pretty good balance with it though. You can still do those things if you really need to, but it has to be marked `unsafe`. This bounds the unsafety to only the parts of the code that actually need it, allowing those parts to be examined more closely to ensure they actually are correct.
> I think Rust strikes a pretty good balance with it though

I don’t like that balance.

For performance-critical things like BLAS and other low-level numerical stuff, C++ is safer than unsafe Rust because standard library and tooling (debug heap, debug allocators, ASAN, etc.) evolved for decades making the language usable despite the unsafety. Another thing, for numerical code you probably need SIMD intrinsics. They were defined by Intel, documentation and other learning resources almost exclusively target C language; C++ got them for free due to compatibility.

For high-level pieces which are not that performance critical, I use C#. Due to VM, it’s safer than Rust. Due to GC and other design choices usability is way better than Rust, e.g. in C# asynchronous I/O is almost trivial with async/await. The runtime is cross-platform, I have shipped production-quality embedded ARM Linux applications built mostly with C#. Unlike Java, it’s easy to consume unmanaged C++ DLLs using C API, or even C++ API: see that library to do that on Linux which doesn’t have COM interop in the runtime https://github.com/Const-me/ComLightInterop

> This is stupid, because the whole point of having a type system or a borrow checker is to tell you when you get it wrong, so you don’t have to memorize how the borrow rules work.

Sweet summer child. Use Haskell for a while and get back to me.

> This means that in order to write C++, you effectively have to memorize the undefined behavior rules, which sucks.

> This means, to be a highly productive Rust programmer, you basically have to memorize the borrow checker rules, so you get it right the first time. This is stupid, because the whole point of having a type system or a borrow checker is to tell you when you get it wrong

I'm not sure how you want to square this circle, you don't want to memorize the rules of UB, but you also don't want for compiler to correct you when you make UB behavior according to Borrow Checker?

The best way in both C++ and Rust is to structure your tree of lifetimes and use other means to achieve your desired goal.

They just don't want the borrow checker errors to only come after type checking. You have to know the rules well because otherwise you're going to finish a big refactor only to find out afterwards it was never going to work.
That's one of their issues. Other issues are borrow virality and stuff for next Rust.
I've written a ton of software, both backend and embedded-like software in C++.

What are people writing that requires such fancy/extensive usage of the borrow checker?

I can't even remember the last time I had to use a shared_ptr... unique_ptr and other general RAII practices have been more than enough for our codebase.

I think OP is likely overusing references. The vast majority of code doesn't need to deal with explicit lifetimes.
OP sounds fairly smart and my first thought is "if OP is struggling then Rust probably isn't for me".

Who is Rust for?

In some ways, people who aren’t heavily invested in other languages to the point that they think in them. Neophytes know less, but they also have fewer things to unlearn.
I don't think it's useful to think there is some single spectrum of intelligence that makes one more or less suited to using Rust.

Rust is for anyone who finds it useful.

> I've written a ton of software, both backend and embedded-like software in C++

Me too.

> What are people writing that requires such fancy/extensive usage of the borrow checker?

The simplest example I can imagine is this: https://en.wikipedia.org/wiki/Matrix_multiplication When your matrices are large and you want it to run fast, you want to parallelize.

Good algorithms (which don’t bottleneck on memory bandwidth) need multiple CPU cores to concurrently store different elements of the same output matrix. Moreover, the elements computed by different cores are not continuous slices, they are rectangular blocks. Such algorithm is not representable in safe rust.

I think it's telling that whenever someone raises concerns about any element of Rust, no matter how constructively, they're always met with a wall of "you must not truly get the borrow checker," or "you're using Rust wrong," or "stop trying to write <C/C++/Java/etc> in Rust!", usually with zero evidence that that is in fact what is happening. There's never anything to improve on Rust, it's always user error / a skill issue. If there ever surfaces any audio of Linus Torvalds and Ken Thompson discussing the pros and cons of the borrow checker, I expect a sea of patronising anime avatars to show up, seeking to explain Rust's invention of the concept of ownership to them.

Rust is really nifty, but there are still (many) things that could be improved in Rust, and we'd all benefit from more competition in this space, including Rust! This is not a zero sum game.

Honestly, I also think many people just want a nice ML-like with a good packaging story, and just put up with the borrow checker to get friendly C-like syntax for the Option monad, sum types with exhaustive matching, etc. This is a use case that could very much benefit from a competitor with a more conventional memory model.

No one thinks there's nothing to improve in Rust, there are lots of features it is missing, some of which are in nightly or on the roadmap. But the borrow checker and the concepts that underpin it are pretty fundamental to Rust and what separates it from other languages. If you like Rust except for the borrow checker, then I would think you don't really like Rust.
> If you like Rust except for the borrow checker, then I would think you don't really like Rust.

The sentence in my original post that this addresses is supportive of the emergence of an alternative language to Rust for people with this use case, so I think we're just agreeing. (Although I wouldn't go so far as to tell others what they do or do not like based on my own ideas of what is essential and what isn't.)

I can't help but feel this is a somewhat veiled complaint about the Rust community instead of anything substantive with the language :/
Veiled? :P

I'd characterise it as a gentle criticism of the way the Rust community tends to react to anything other than effusive praise.

Rust is a nifty language, albeit with room for improvement, that falls into the (sadly overpopulated) category of 'neat thing, somewhat obnoxious fan club'.

There are improvements to the borrow checker that is in the roadmap for Rust 2024. So it's not like the "community" is claiming there are no issues
I am A-OK with someone not liking Rust. I do, but it’s still only my 3rd-most used language behind the Python and TypeScript I write at work.

It’s just that time after time I’ve heard people criticize Rust because they were, in fact, trying to write their pet language in Rust. It’s similar to how many complaints I’ve heard about Python because “it’s weakly typed”. What? Feel free not to like either of them, but make it for the right reasons, not because of a misunderstanding of how they work.

Now, the author of this post may be doing everything right and Rust just isn’t good at the things they want to use it for. The complaint about constantly bumping against the borrow checker leads me to wonder.

> It’s similar to how many complaints I’ve heard about Python because “it’s weakly typed”. What? Feel free not to like either of them, but make it for the right reasons, not because of a misunderstanding of how they work.

Are you sure you're not just being harsh to people whose grasp of CS vocab is weaker than yours? If someone tells me that Python is 'weakly typed', I translate it in my head to 'dynamically typed', and the rest of their complaint generally makes sense to me, in that the speaker presumably prefers static typing. Which is a valid opinion to hold, not necessarily the result of any misunderstanding.

Reasonably sure. If it’s clear they actually mean dynamically typed, fine. That’s down to preference, and I won’t say they’re wrong any more than I’ll argue that chocolate is better than strawberry.

However, I’ve heard lots of utterly wrong criticisms of Python (and Rust and…) that were based on factual misunderstandings and not just a vocabulary mistake.

> whenever someone raises concerns about any element of Rust, no matter how constructively,

I don't think that it is a very constructive article. The author's critique of Rust raises questions like "how to do it better" but there are not answers.

> they're always met with a wall of "you must not truly get the borrow checker,"

Yeah, it is frustrating in this case particularly. The author openly states that he doesn't want to learn all the quirks of the borrow checker, and people respond to it with "you just don't get the borrow checker". I can see how this answer could be helpful, but if it was expanded constructively, if there was an explanation how it can become easy to deal with problems the author faces if you understood the borrow checker. OTOH I cannot see how such an argument can be constructed without a real example with the real code and the history of failed changes to it.

I personally feel, that the borrow checker is simple, if you got it. And the author's struggles just go away, if you got it. You can easily predict what will happen if you try this or that changes to the code, and you know how to do something so the borrow checker will be happy. But I cannot elaborate and to make it clear how it works.

> I think it's telling that whenever someone raises concerns about any element of Rust, no matter how constructively (...)

I'm far from a Rust expert, but to me if someone is whining about how it is hard to track lifecycle rules of an object because they are passing it through long chains of function calls across all sorts of boundaries, what this tells me is that you're creating your own problems that you could avoid if you simply passed the object by value instead of by reference. I mean, if tracking life cycles is a problem then why not prevent it from being a problem? Not all code lives in the hot path. I'm sure your performance benchmarks can spare a copy somewhere.

> I mean, if tracking life cycles is a problem then why not prevent it from being a problem?

So you're suggesting that people should just wrap everything in Arc or make copies everywhere to avoid lifetimes? At that point why not just use Java/OCaml/Swift/your-favourite-GC-lang?

> So you're suggesting that people should just wrap everything in Arc (...)

You're the only one who managed to come up with this nonsense. No one else did, and clearly you did not pick that from what I wrote because I definitely did no wrote that.

Please refrain from slippery slope fallacies.

> zero evidence

you mean except the original poster often implying exactly that in their articles or the personal experience of the commenter that nearly always whenever they ran into borrow checker problems it was due to exactly that reason

> There's never anything to improve on Rust, it's always user error / a skill issue.

RFCs get accepted and implemented nearly every week, like in any language there is always a lot to improve

the problem is that the complains of such articles are often less about aspects where you can improve things but more about "the borrow checker is bad" on a level of detail which if you consider the borrow checker a fundamental component basically is "rust is fundamentally bad"

Yes, and that's why the comments usually are in some sense "you don't use the BC right", because they don't want a BC. And that's fine, but you can't blame Rust for this
Can't say I agree, or that this matches my experience of writing Rust.

I don't memorise how it works, I've just learnt what it rejects and why, and this in turn becomes clear as to why it's rejected that. Very rarely do I find myself going "oh bother, now I suddenly need to `Rc` or `Arc` this, I suspect because I've just gotten into the habit of suspecting when I anticipate things will run afoul and structuring things from the get-go to avoid that. Admittedly, I'm not writing absurdly low-level code.

I wonder if the authors grounding C++ is making life harder for them? Often when I've had to teach people Rust, getting them to stop writing {C/C#/Java}-but-in-Rust is the first stop on the trail to "stop fighting and actually enjoy the language". Every language has its idioms, just because you can, doesn't mean you should.

I do have to ask, I have worked in codebases which used lifetimes and didn't lean into Rc/Arc and vice-versa.

I used to think Arc/Rc was a shortcut to avoiding the borrow checker shenanigans, but have evolved that thinking over time.

You do mention it in your comment so wondering if you have anything to share about it

If you aren't writing low level code, why not use a GC language?
Great type system, great performance, great packages, great tooling, nice high-level API’s that produce low overhead code, I’m most familiar in Rust now.

I’ve had more than enough unpleasant experiences with write-runs-breaks-at-runtime style languages. I don’t like writing them, I hate having to support them in prod as they give me constant-persistent-stress. I hate what idiomatic C# is, and how much ceremony there is to read and write it. Java is worse. Go’s type system is too anaemic for my tastes. Haskell is nice, but gets a bit academic and lacks some day-to-day niceties. Kotlin is supposed to be nice, but again, we’re getting maybe 50% of Rust type system features, and you’re basically just piggybacking on Java/JVM which I hated dealing with previously. IDK what else that leaves in the mainstream. I used to play around with Nim, and that was quite nice though.

F# :)

Expression-oriented, HM type inference with gradual typing, faster than other FP languages, can even reach for low-level bits, or write extra glue code in C# which is more pleasant at low-level imperative code.

Not sure what exactly you refer to as idiomatic C#. If it has too much ceremony chances are it’s anything but!

Yeah I’ve used F# before! It was pretty good, some solid features and nice experience. It just falls into a bit of weird place IMO? You have to rely on writing/using C# to fill any holes, and I really dislike that language/ecosystem, and why split between 2 lands when I can just get the same HM type system, similar-enough principles, better perf and no MS taint.

Edit: I do love the ML style syntax though, Haskell, F#, Dhall are awesome, I wish it were more readily accepted.

The hatred of .NET (and C#) is unfortunate, irrational and unwarranted. I ended up unfortunately resorting to just thinking less of engineers that have it, because they can’t update their priors (“it was slightly inconvenient 8 years ago so it must be bad today surely”) and distinguish between Microsoft’s other products and policies and .NET itself.
I believe that’s shortsighted. I’ve been adjacent to any number of Microsoft rugpulls over the years (Visual FoxPro.NET any day now, promise!). Their are enough non-MS languages that C#’s never tempted me. I don’t think it’s a bad language and don’t think poorly of you for using it, but I feel much better about the long-term prospects of Rust and wouldn’t likely build a new business on C# today.
It's not. It is shortsighted to blindingly hate thing X over Y instead of considering circumstances and understanding how big corporations work, or understanding the nature of the project you make use of (after all, the entirety of .NET is MIT) and just how much worse most alternatives are. There is a wealth of mainstream languages that are decently usable, which I wouldn't touch with a ten-foot pole as a main choice still because they carry a significant downgrade in one or another area, that .NET does not compromise on.
I’m not blindly disliking anything.

I recognise C# as useful, but _personally _ dislike it because of the sprawling, OOP-heavy, ceremony-heavy core it encourages you to write. Half my dev friends write C# for their day jobs, half my workplaces have had me dealing with it. It’s not an opinion borne from unfamiliarity or knee jerk reaction; my distaste for MS is just the cherry on top.

A theme I’m noticing more frequently as Rust gains popularity is people trying to use Rust even though it doesn’t fit their preferred way of coding.

There is a learning curve for everyone when they pick up a new language: You have to learn how to structure your code in ways that work with the language, not in the ways you learned from previous languages. Some transitions are easier than others.

There should come a point where the new language clicks and you don’t feel like you’re always running into unexpected issues (like the author of this article is). I might have empathized more with this article in my first months with Rust, but it didn’t resonate much with me now. If you’re still relying on writing code and waiting for the borrow checker to identify problems every time, you’re not yet at the point where everything clicks.

The tougher conversation is that for some people, some languages may never fully agree with their preferred style of writing code. Rust is especially unforgiving for people who have a style that relies on writing something and seeing if it complies, rather than integrating a mental model of the language so you can work with it instead of against it.

In this case, if someone reaches a point where they’re so frustrated that they have to remember the basic rules of the language, why even force yourself to use that language? There are numerous other languages that have lower mental overhead, garbage collection, relaxed type rules, and so on in ways that match different personalities better. Forcing yourself to use a language you hate isn’t a good way to be productive.

> A theme I’m noticing more frequently as Rust gains popularity is people trying to use Rust even though it doesn’t fit their preferred way of coding.

I could've expressed the sentiment in this blog post back when I started playing with Rust ~2016. Instead, I ended up learning why I couldn't pass a mutable reference to a hashmap to a function I'd looked up via that hashmap (iterator invalidation lesson incoming!).

The kind of bug I was trying to add exists in many languages. We can only speak in general terms about the code the blog post is talking about, since we don't have it, but couching it in terms of "doesn’t fit their preferred way of coding" misses that the "preferred way of coding" for many people (me included) involved bugs I didn't even realise could exist.

> The kind of bug I was trying to add exists in many languages

Any example except C++? BTW, the closest thing possible in C# is modifying a collection from inside foreach loop which iterates over the collection. However, standard library collections throw “the collection was modified” exception when trying to continue enumerating after the change, i.e. easy to discover and fix.

> modifying a collection from inside foreach loop

This is exactly what the mutable map pointer was for, for the function to be able to modify the collection; C++ would result in potentially iterating garbage, C# it sounds like would throw an exception (and so show the design wouldn't work when I tried to test it), Python definitely didn't do a graceful thing when I tried it just now. And if I had a collection struct in C, I'm sure I could've done some horrible things with it when trying.

The best of those outcomes is C#, which would've shown me my design was bad when I ran it; that could be as early as the code was written if tests are written at every step. But it could be quite a bit later, after several other interacting parts get written, and you rely on what turns out to be an invalid assumption. For Rust, the compiler alerted me to the problem the moment I added the code.

FTR I ended up accumulating changes to the map during the loop, and only applying them after the loop had finished.

EDIT: Python did do something sensible: I didn't expect pop'ing an element from the list to echo the element popped in the REPL, and got a printed interleaved from front to back, which does makes sense.

Python defined the semantics of modifying lists while iterating over them, which is better than c/c++ just calling it undefined behavior, but I've basically never seen it not be a bug. Either I end up creating a copy of the list to iterate over, or figure out a way to defer the modifications, or write a new list. Imo the c# /Java behavior of detecting and throwing is probably the best option for non borrow checked languages.
> Any example except C++?

Java for sure.

> However, standard library collections throw “the collection was modified”

Java is similar, but the exception is done on a "best effort" basis, and is not guaranteed.

Python will raise a runtime exception if you modify the dict you are iterating over. You can work around that by copying a snapshot of the whole thing or just the keys and iterating over that.

C++ will laugh in invalidated iterators.

Of course you can erase from the container you are iterating over, but you have to make sure to continue the iteration using the iterator returned from the erase function and avoid storing copies of .end()

I'm pretty sure most languages just make the reasonable assumption that you want to map to the object so it uses the pointer for hashing and not actually hash its value.
(comment deleted)
And yet, such mutation is perfectly safe as long as you only change the value of existing keys. But you can't do that in Rust.
You would use interior mutability, putting things in Cells in the map.
> you’re not yet at the point where everything clicks

This just seems to be the standard excuse I read any time someone has a critique of Rust.

> This just seems to be the standard excuse I read any time someone has a critique of Rust.

The guys who parrot this blend of comments don't seem to be aware that cognitive load is a major problem, not a badge of honor.

Learning the language fundamentals is not "cognitive load" more so for rust than c.

With c you also need to understand pointers, manual allocation, volatility, is that bad?

What did the Rust community expect to happen if they so strongly and often claimed that Rust is easy to learn and user-friendly?

And how did we get from that to “for some people, some languages may never fully agree with their preferred style of writing code”?

If a C++ programmer (ideal Rust learner) says four years in that the ergonomics of Rust are bad, then based on my own experience I will believe them.

I don’t write something to see if it compiles either, I design every single line of code. But with Rust (and a lesser extent C++) a lot of that design is for memory safety and not necessary for the algorithms.

Rust is easy to learn and user friendly. But if you are stuck in your ways and insist on writing code exactly like you did in another language you will have a hard time. This is true for every language on earth. Rust will refuse to compile, whereas other languages give you much more freedom to not use them as they are meant to.

> If a C++ programmer (ideal Rust learner)

There is no ideal learner.

> Rust is easy to learn and user friendly. But if you are stuck in your ways (...)

It's high time that people in the Rust community such as you just stop with this act.

The Rust community itself already answered that they find "Rust too difficult to learn or learning will take too much time" as a concern to not use Rust. The community also flagged Rust being too difficult as one of the main reasons they stopped using Rust.

https://blog.rust-lang.org/2024/02/19/2023-Rust-Annual-Surve...

Anyone who has managed to become proficient in C++ is smart enough to be proficient in Rust. The only too difficult part is adapting to the rust way of doing things and some refuse to.
There is a saying among Haskellers that "Haskell has the steepest unlearning curve". Sounds like Rust is similar.
Everybody who has managed to become proficient building a school is smart enough to be proficient in building a library.

So building libraries must be easy, that's totally why you can become an architect over night ... What a stupid argument you delivered

> Anyone who has managed to become proficient in C++ is smart enough to be proficient in Rust.

I'm not sure you're paying attention. The people who are saying Rust is too hard are the Rust community itself. They said so in Rusty's annual survey. The ones who participate in it are Rust users who feel strongly about the topic to participate in their governance.

It's Rust users who say Rust is too hard. There is no way around this fact.

> Anyone who has managed to become proficient in C++ is smart enough to be proficient in Rust.

I agree completely. But I'd reword your statement slightly as "Anyone who has managed to become proficient in the language that is notorious for being one of the most complicated and convoluted languages in existence, with the most footguns per line of code and a CVE record that proves its difficulty, ..."

It changes the conclusion just a tad.

Rust is not easy to learn. Stop saying that. It is hard. It is harder than C#, harder than Python, harder than Java, harder than PHP, harder than JS, Harder than TS.

Saying it is easy to learn is just delusional and does a disservice to the language. Rust has many advantages, but trying to get people learning the language by lying about why they should learn it is just dumb.

You just seem to have missed the point the author was making. I will try to clarify it: when you find out that a decision you made early on didn't pan out and you're forced to change the lifetime of some data, this will incur major refactoring in Rust and that will cause you to lose a lot of time. It's nearly impossible to avoid mistakes like this, not because you don't know Rust enough, that's almost completely irrelevant... it's because you just can't predict the direction your design will go to after many iterations and changes in requirements, which are unavoidable in the real world.

Replying to that with "Rust is easy to learn" just makes it sound like you didn't even understand what you're trying to reply to.

> What did the Rust community expect to happen if they so strongly and often claimed that Rust is easy to learn and user-friendly?

No one really claimed it was easy to learn. Find me one Rustacean that claims Rust is as easy as Python/Ruby/Go.

But it is higher level language with decent ergonomic and in such way it can be interpreted as user-friendly.

> If a C++ programmer (ideal Rust learner)

If Java programmer (me) can learn Rust in a year enough to contribute to Servo development, you (ideal Rust learner) should have no problem either.

Plus Google saw about 6-month to get people up to speed with Rust.

> No one really claimed it was easy to learn.

It's a great example of unintended comedy the fact that the comment right below yours is literally "Rust is easy to learn and user friendly."

https://news.ycombinator.com/item?id=42162676

Sure but that's a no one. Same as me. Find someone on Rust team saying that. Or someone teaching Rust constantly.

I can see user friendly. It's got a solid ecosystem around it, and compiler is super helpful with errors.

But easy to learn it ain't.

> C++ programmer (ideal Rust learner)

Well there's your problem. Rust does look like a semi-colon language, that's intentional, but if that's all you understand you're probably going to struggle.

The "ideal Rust learner" would have an ML, such as Ocaml or F#, maybe some Lisp or a Scheme something like that, as well as a semi-colon language like C or Java not just the weird forced perspective from C++

One experiment I probably won't get to try is to teach Rust as First Language for computer science students. Some top universities (including Oxbridge) teach an ML as first language, but neither teaches Rust because of course Rust is (relatively) new. The idea is, if you teach Rust as first language your students don't have to un-learn things from other languages. You can teach the move assignment semantic not as a weird special case but as it it really is in Rust - the obvious default assignment semantic. I pass a Goose to a function, obviously the Goose is gone, I gave it to the function. Nobody is surprised that when they pass a joint they don't have the joint any more, so why are we surprised when we pass a String to a function and the String is gone now? And once we've seen this, the motivation for borrows (reference types) is obvious, often we don't want to give you the string, we just want to tell you about a string that's still ours. And so on.

> Well there's your problem. Rust does look like a semi-colon language, that's intentional, but if that's all you understand you're probably going to struggle.

This is a silly point to make. What makes a C++ programmer a C++ programmer is not an uncanny ability to find a semicolon on the keyboard. It's stuff like using low-level constructs, having a working mental model of how to manage resources at a low level down and how to pass and track ownership of resources across boundaries. This is not a syntax issue.

It's absurd. You have people claiming that Rust is the natural progression for C++ programmers because their skillsets, mental models, and application domain overlap, but here are you negating all that and try to portray it as a semicolon issue?

Oh! Alas the apparent syntax similarity is misleading. If you prefer imagine I wrote "C-like language" anywhere that I wrote "semi-colon language". It's not really about the semi-colon, indeed the semi-colon isn't even doing the same thing and that's a clue†. You see, the typical semi-colon languages you'll have used such as C++ or Java have these "everything is a machine integer" type systems which were a small step up from the "Who needs a type system" of their predecessors but aren't anywhere close to what you'd expect in an ML or a functional language.

So this means there's a substantial learning curve, or, if you try to just keep writing C++ even though you're in Rust, an impedance mismatch. It looks superficially like the thing you're used to, but that's not what it is.

† In Rust the semi-colon is turning your expression into a statement, but in the semi-colon languages it's a separator, everything is a statement anyway. So while a rust program might say let x = if k > 3 { plenty(k) } else { too_few() }; in the semi-colon languages there's a whole separate ternary operator provided to do this trick - in fact they don't have any other ternary operators and so they often call this "the" ternary operator which is very funny if you come from a language which has the multiply-accumulate operator...

That's an... uncharitable interpretation of the phrase "semi-colon language".

The comment made sense to me: it's not at all about semicolons, but the semicolon isn't a bad marker to distinguish between language families. It's closely related to expression- vs statement-focused languages, which if you squint is similar to the distinction between imperative and functional languages. Though the latter distinction is less relevant here, I'm just using it to point out that semicolon syntax is suggestive of semantics, even if it is itself just a superficial bit of syntax.

You are correctly describing the awareness of resource management as an important characteristic, but that resource management is heavily intertwined with how values flow through the language syntax, which brings us back to expressions vs statements.

(comment deleted)
Like many of these sorts of critique articles, I can see the author's pain point and empathize, but don't fully agree. Yes, it is true that if you aren't careful you can end up with a design that doesn't work and has to be redesigned. And yes, I do agree when this happens it can be very frustrating (and a time suck). However, in my experience, and it probably depends what type of code one writes, it doesn't happen enough to fully mar the experience of an otherwise very productive language. If I had to guess, I would say this happens to me maybe 1 day in 30. Not great, but not catastrophic either.

If I'm working on a section of code the relies heavily on borrowing and lifetimes, I will typically work up a prototype without all the functionality just to ensure I have a workable design before going back to fill in the rest of the code. This is probably why I don't tend to hit it all that often. It would be ideal if this wasn't necessary, but Rust has all sorts of other awesome features that make this something worth enduring.

As a long time Rust user, I more or less agree that it's a pain. And yeah, when working on a big project refactoring code, it's difficult to know ahead of time if the borrowing pattern you _think_ will work will actually work.

Of course, that's always the trade off with Rust. You're trading a _lot_ of time spent up-front for time saved in increments down the road.

As a concrete example, I'm in the process of building up a Rust database to replace the Postgres solution one of my applications is using. Partly because I'm a psycho, and partly because I've gotten query times down from 20 seconds on Postgres to 50ms with Rust (despite my best efforts to optimize Postgres).

Being a mostly ACID, async database, this involves some rather unpleasant interactions with the Rust borrow checker. I've had to refactor a significant portion of the code probably five times by now. The lack of feedback during the process is a huge pain point, as the article points out (though I'm not sure what the solution to that would be). Even if you _think_ you know the rules, you probably don't, and you're not going to find out until 2 hours later.

The second most painful point has to be the 'static lifetime, which comes up a lot when dealing with threading and async. For me it's when I need to use spawn_blocking inside an async function. Of course, the compiler has no way of knowing _when_ or _if_ spawn_blocking will finish, so it needs any borrows to be 'static. But in practice that means having to write all kinds of awkward workarounds in what should otherwise be simple code. I certainly understand the _why_, and I'm sure in X years it'll be fixed, but right now ... g'damn.

That said, the borrow checker _has_ improved. I think my last major Rust project was before the upgraded borrow checker, which wasn't able to infer lifetimes for local variables. So you had to throw a lot of stuff inside separate blocks. We also have a lot more elided lifetimes now. Just empirically from this project I'd say me and the borrow checker only had about 30% of the fisticuffs we did in the past.

Personally, I think the tradeoff is worth it. It won't be for everyone, or every project. But 20s to 50ms query time, with a ton of safety guarantees to ensure the valuable data running through the database is well cared for? Worth every line of refactored code.

Asides:

* The project in question: https://github.com/fpgaminer/tagstormdb

* I also replaced some of my large JSON responses with FlatBuffers. FlatBuffers is a bit of a PITA, but when you're trying to shuffle 4 million integers over to the webapp, being able to do almost 0 decoding on the browser side, and get them as a Uint32Array directly is gold.

* It's a miracle I got away with the search parser in the project. I use Pest, and both the tree it spits out and the AST I build from it hold references. Yet sprinkling a little 'a on the impl's and struct's did the trick.

* Dynamic dispatch has also improved, as far as I can tell, which used to always involve some weird lifetimes if the return values needed to borrow stuff.

* ChatGPT o1 is a lot better at Rust then 4 or 4o. I've gotten a lot more useful stuff out of o1 this time around, including less hallucinations. Still weaker than Python/TypeScript/etc, with maybe 2-3 compile errors that need to be fixed each time. But still better. Sonnet completely failed every time I tried it :/ (Both through Copilot and the web). o1 in Copilot _could_ be amazing, since I can directly attach all my code. But the o1 in Copilot _feels_ weaker. I'm fairly sure the 4o Copilot uses is finetuned, and possibly smaller, so it too always felt weaker. Seems like o1 is the same deal. Still really useful for the Typescript side of things, but for Rust I had to farm out to the web and just copy pas...

Sounds familiar. I'm a relative beginner at Rust, but I've started feeling like I get the hang of the borrow checker rules and can get something close to right on my first writing. Finally. Ok, maybe 2nd or 3rd, but definitely before the dozenth!

Or at least I thought I did, until I launched into a project that mixes async and threading. That's where I hit a wall. What it's complaining about makes sense to me, but how to fix it does not -- partly because the async and threading come from libraries that I'm trying to stitch together. They necessarily have their own idiosyncratic ways of dealing with the issues, and as a beginner I don't even fully understand the problem they're solving let alone their solutions.

(For the record, I'm basically trying to force an unholy matrimony of matrix-sdk + tokio + pyo3 + pyo3-asyncio. You can take Python's GIL to grab out values to work with, but then if you want to do an async function then you'll have to release the GIL to stuff things into a future, and... well, if I fully understood the problem then I wouldn't be here whining about it, would I?)

I’ve been wrestling with Swift’s region isolation checker recently and had a similar experience.
Daily reminder that every Turing complete language has undefined behavior.
What? That’s not true. Turing machines themselves do not have undefined behavior.
Halting problem?

The overall point is it’s impossible to make a perfect spec. Rust doesn’t even have a spec, so your behavior is whatever the compiler gives you, which is C++ UB too.

Undefined behavior means something specific in the language specification world.

The language specification, or standard, guarantees certain things about the behavior of programs written to the specification. "Undefined behavior" means "you did something such that the guarantees of this specification no longer apply to your program." It's pretty much a worst case scenario in terms of writing programs. The program might do... anything. Fortunately, in reality it happens all the time and programs often keep behaving close enough to what we expect.

Turing completeness is unrelated to that sense of "undefined behavior".

I understand and my point is rust has no “ub” only because there is no spec, not because it avoids inherent computing problems.

> halting

They are not entirely unrelated. C++ UB is often things that would be very difficult to detect.

For example infinite template recursion is undefined. Specifying any other behavior is impossible due to halting problem.

Another example: a system might be able to detect out of bounds pointer deref, or maybe not. Same with signed integer overflow.

> I understand and my point is rust has no “ub” only because there is no spec, not because it avoids inherent computing problems.

Well, your point is wrong because UB is not an inherent computing problem. That's what the post above tried to explain.

Many forms of UB are inherent to C-like languages, but languages don't have to be C-like.

> For example infinite template recursion is undefined. Specifying any other behavior is impossible due to halting problem.

A language can avoid this by not having infinite template recursion.

C++ currently allows infinite recursion at the language level, while acknowledging that compilers might abort early and recommending that 'early' is a depth of 1024 or higher. But a future version could bake that limit into the language itself, removing the problem.

> Another example: a system might be able to detect out of bounds pointer deref, or maybe not. Same with signed integer overflow.

A language can avoid out of bounds deref in many ways, one of which is not allowing pointer arithmetic.

Signed integer overflow is trivial to handle. I'm not sure what problem you're even suggesting here that the person in charge of the language spec can't overcome. C++'s lack of desire to remove that form of UB is not because it would be difficult.

> A language can avoid this by not having infinite template recursion.

How does it know whether a definition is infinitely recursive? This IS the halting problem.

> But a future version could bake that limit into the language itself,

In other words, take away the Turing completeness of templates. Which goes back to my original comment.

Also note that limiting recursion hurts real world use case (types getting arbitrarily complex in a program over time) in favor of theoretical benefit (now you can say it’s not UB).

> pointer Deref

Let me explain again. In a language with pointers checking whether a deref is valid requires comparing every address to every allocation bounds. That’s ridiculously expensive.

The only solution is to take away pointers (Java, C#, etc) OR do what C does, crash on obviously bad derefs. Since “obviously bad” depends on the implementation (maybe you are a safety sadist and you want 2000 instructions per deref) the standard cannot guarantee any behavior. Maybe it crashes, maybe you get “lucky” and it doesn’t notice.

The only way to avoid UB is to limit expressiveness (pretend addresses don’t exist), and all Turing complete languages have UB.

I have more responses, but you’re not grasping the ones I already made.

> In other words, take away the Turing completeness of templates. Which goes back to my original comment.

Template halting is only a correctness issue because it's done at compile time. Turing completeness is not a problem in general, and limiting the amount of computation at compile time is fine.

> The only solution is to take away pointers (Java, C#, etc) OR do what C does

Something along those lines.

> The only way to avoid UB is to limit expressiveness (pretend addresses don’t exist), and all Turing complete languages have UB.

The only way to avoid UB is to limit expressiveness, and NOT all Turing complete languages have UB.

Pointers are a big thing to restrict for a safe language. But you really don't have to do that much else. Whether a program halts or doesn't halt at runtime isn't a safety issue, there's no UB involved. It just runs indefinitely.