> coding in Rust is like playing some kind of intricate puzzle game. Tricking the compiler into accepting your code
That is mostly only true for very early beginners.
There are certain patterns you have to learn and understand, especially for developers not accustomed to thinking about lifetimes ( which applies to most developers that only have experience with GC languages).
And sometimes you do have to battle the compiler, even with experience.
But most of that goes away pretty quickly once the language clicks for you.
After that Rust can still feel restrictive, but that's because there are very few languages that enforce as much correctness at compile time.
That very well can mean that Rust just isn't a good fit for certain domains - which is perfectly fine!
> To be honest, everything in Rust looks a bit ugly to me.
I write a lot of Rust code at work, and I admit that it can sometimes be pretty noisy. There are several major contributors to this:
1. Rust offers fine-grained control over pass-by-value, pass-by-reference, and pass-by-mutable reference. This is great for performance. But it also adds a lot of "&" and "&mut" and "x.to_owned()" clutter everywhere.
2. Rust provides support for generics (aka parameterized types). Once again, this is great for performance, and it also allows better compile-time error detection. But again, you wind up adding a lot of "<T>" and "where T:" clutter everywhere.
3. Usually, Rust can automatically infer lifetimes. But every once in a while, you want to do something messy, and you end up needing to write out the lifetimes manually. This is when you end up seeing weird things like "'a". But in my experience, this is pretty rare unless I'm doing something hairy. And if I'm doing something hairy, I'm just as happy to have more explicit documentation in the source code.
Really, the underlying problem here is that (a) Rust fills the same high-control, high-performance niche as C++, but (b) Rust prefers explicit control where C++ sometimes offers magic, invisible conversions. (Yes, I declare all my C++ constructors "explicit" and avoid conversion operators.)
Syntax is a hard problem, and I've struggled to get syntax right for even tiny languages. But syntax for languages with low-level control is an even harder problem. At some point, you just need to make a decision and get used to it.
In practice, I really enjoy writing Rust. It's definitely not as simple as Ruby, Python or Go. But it fills a very different ecological niche, with finer-grained control over memory representations, and support for generics.
> and the first couple of introductory chapters should use only unsafe code (I'm only half joking on both points)
It's really at least half-true. As far as I understand the libraries of Rust itself can't have everything written in "safe" code.
The hard part is to win the potential programmers to use the safe code as much as they can but to understand where the limits are.
And that is the hardest thing to do as a lot of people prefer simplifications the kind of "everything must be done the X way" etc. I call that a "religious programming" approach and the programming world has it in immense amounts.
> Frankly, I am not even sure that many Rust programmers could describe what a control flow graph is, let alone that they somehow learn the exact rules and then apply them. People don't learn programming languages that way, just like they don't learn human languages that way. It is largely a practice of building up intuitions and testing them out.
I think a lot of programmers want to be able to understand how the language works in terms of formal rules and predict whether a given piece of code will pass the compiler without guess-and-check. Indeed, it seems like a pervasive complaint about Rust is that it requires too much of this “try things until it compiles” approach, and makes it hard to develop a clear mental model of how the borrow checker works.
> This arguing with the compiler is really only while you are learning Rust and it's nuances. For me the first two weeks were this way, then I started thinking more in Rust terms. Now I rarely deal with the borrow checker at all, but I do end up in some complex type situations.
It just means you've learned to head off problems before they occur, that does not mean the problems you're heading off should be there.
> It was a weekend learning project, and all I wanted to do was create a freshman level data structure from scratch.
It's worth mentioning that the implementation difficulty of data structures in Rust is not representative of the general difficulty of programming in Rust. Just because this is hard doesn't make it a bad language. Rust's philosophy is that you deal with the trouble of writing unsafe code each time you need a new low level abstraction, and when done, you don't have to worry about it again.
Also, writing generic datastructures in C++ without falling afoul of UB, and/or getting destruction semantics right can be quite tricky. Rust ... really doesn't have additional issues here; raw pointers in Rust work the same way as in C++, except they're a tad verbose. The only difference is that in Rust you probably want to provide a safe API, whereas in "freshman C++ datastructures" the datastructure does not have the unsafety neatly encapsulated. This is a hard task in both Rust and C++, especially with generic datastructures.
Writing datastructures isn't really a common task. It's a "simple" task because it's simple in C and C++, but here's no real reason it has to be in Rust. You can write inefficient datastructures in 100% safe Rust without too many problems, much like you can implement datastructures in regular-joe Java. Writing efficient, low-level datastructures can be pretty hard, but like I said it's a rare task so not much of an issue.
In general I advise folks to not write unsafe Rust till they are sure that they have a clear grasp of safe Rust. But if you want I can have a look at what you tried and help you improve your design. I'm very interested in making such things more accessible (on one hand, the Rust community doesn't really want to encourage the use of unsafe code, on the other hand, sometimes you need it and it would be nice if it was easier for people to use when that situation arises) so learning what people stumble on is important to me.
> Unfortunately, I don't think they're very concerned about my particular use cases (and maybe they should
>
> I think all this says is that you don't write very much of the kind of code which benefits from generics. You can dismiss my point of view, but some of us do use them a lot, and it's one of the driving reasons I use C++.
It seems like you're using generics the way you use templates for metaprogramming in C++. I totally get how powerful TMP is (I love to use it myself, though I can get carried away), but be aware that Rust is a different language and you have to approach it differently. It's important to consider the limitations of the system when designing a solution -- if I designed a solution for C++ and implemented it in Rust, I'd hit problems in the last mile and find it very annoying to make it work (using macros or something to fill in the gap). However, if you design the code from the start with Rust's advantages and limitations in mind; you may come up with a different but just as good system. This is something I hit every time I learn a new language. I hit it with Rust, Go, and many years ago, C++. So it may help to approach the language with a fresh mind.
Yes, coherence is painful. I seem to hit coherence issues pretty rarely myself, but they're annoying when they happen. There's work going on to improve these pain points (specialization!), but it's overall not that big a problem.
I think an example of what you tried might help. Preferably a more holistic example, condensed minimal examples tend to hide instances of the XY problem.
> Disclosure: I don't really know what an "overlapping instance" means in this context.
cases where you have more than one implementation that make sense for a given call. C++ solves this by allowing things to overlap and introducing overload resolution rules. Rust solves this by not allowing overlap.
> but I'm guessing the memory foot print would be at least twice what it is in C++. (I should measure that though.)
Go does make extensive use of the stack and has decent escape analysis, so it might not be that bad, really. But measuring it is probably the best way to go here.
> Some other time it's because the standard library doesn't have a trait for a commonly implemented method, so I can't write a generic function to call that method.
Do you have examples? In some cases generic methods are outside the stdlib in different crates, e.g. num_traits.
> But writing safe C++ is much, much, much more complex than than writing okay-ish C++. The way I see it, Rust has basically taken a lot of the best practices required to write sane C++ (e.g. RAII) and formalized them in a way where the compiler can enforce them.
A concrete example that I've run into recently when trying to write C++ code. I figured that, for safety reasons, I needed to make my type be move-only. I then had to spend about two hours trying to figure out why the program was blowing up. The reason was that I was reusing the variable after moving from it, and the compiler never gave any warning (even on -Wall -Werror) telling me that what I was doing was wrong. In Rust, the same situation would be a compiler error.
> The compiler literally takes all the mental overhead away.
Increasing the strictness of the language has one effect: decreasing the number of potential solutions for a given problem.
If the restrictions are carefully chosen (like they are in rust) this leads to generally safer code. But don't fool yourself, the restrictions don't merely generate new solutions - they reject the ones that don't pass the tests.
A more extreme example is formal theorem provers. Carefully constructed proofs will take a lot of effort, but it will also make you confident that the code does what it needs to do.
The rust borrow checker is just a more restricted theorem prover that doesn't touch the logic, it just deals with the memory side of things. It's indeed very helpful in trying to explain what's wrong (and even suggest fixes), but it doesn't take the programmer overhead away.
In a more complex system rust inevitably makes it harder to come up with solutions. It will reject valid code just because it can't prove it's right, not because it's actually wrong. As a programmer, you're going to come up with such solutions, and while in time you get more used to writing code that rust likes, and rust too gets better at accepting correct solutions, you're going to have to fight the borrow checker sometimes.
I don't have a ton of experience with rust, but I encountered cases where equivalent C++ code would've worked just fine, but I had to change it because rust didn't like it.
Rust is an amazing language, but it definitely doesn't 'take all the mental overhead away'.
> They’re saying that a lot of the restrictions makes things much harder than other languages. Hence the general problem rust has where a lot of trivial tasks in other languages are extremely challenging.
Like what? So far the discussion has revolved around rewriting a linked list, which people generally shouldn't ever need to do because it's included in the standard lib for most languages. And it's a decidedly nontrivial task to do as well as the standard lib when you don't sacrifice runtime overhead to be able to handwave object lifecycle management.
> No need to get defensive, no one is arguing that rust doesn’t do a lot of things well.
That's literally what bsaul is arguing in another comment. :)
> You’re talking up getting a safe implementation in C, but what matters is “can I get the same level of safety with less complexity in any language”, and the answer is yes: Java and c# implementations of a thread safe linked list are trivial.
Less perceived complexity. In Java and C# you're delegating the responsibility of lifecycle management to garbage collectors. For small to medium scale web apps, the added complexity will be under the hood and you won't have to worry about it. For extreme use cases, the behavior and overhead of the garbage collector does became relevant.
If you factor in the code for the garbage collector that Java and C# depend on, the code complexity will tilt dramatically in favor of C++ or Rust.
However, it's going to be non-idiomatic to rewrite a garbage collector in Java or C# like it is to rewrite a linked list in Rust. If we consider the languages as they're actually used, rather than an academic scenario which mostly crops up when people expect the language to behave like C or Java, the comparison is a lot more favorable than you're framing it as.
> If I wanted I could do it in c++ though the complexity would be more than c# and Java it would be easier than rust.
You can certainly write a thread-safe linked list in C++, but then the enforcement of any assumptions you made about using it will be a manual burden on the user. This isn't just a design problem you can solve with more code - C++ is incapable of expressing the same restrictions as Rust, because doing so would break compatibility with C++ code and the language constructs needed to do so don't exist.
So it's somewhat apples and oranges here. Yes, you may have provided your team with a linked list, but it will either
(1) Perform less efficiently, due to needing the GC to check whether to free things
(2) Require more expertise to use safely, due to C++ being incapable of expressing constraints
> Rust has increased complexity of some “simpler” things to reduce the overall complexity of larger systems. This is an ok choice.
This is sort of right and sort of not.
In the context of Java and C#, Rust hasn't "increased complexity", it makes it explicit rather than paying runtime cost to try and hide it. In the context of C++, Rust hasn't "increased complexity" either, it makes it mandatory to deal with things that C++ lets slide.
I'd look it more as Rust requires a higher degree of confidence in the code. Rust is more likely to take the programmer to task about "What did you mean?" or "Are you sure about that?". It's like doing a code review with an extremely pedantic developer.
As a product of this, the performance is better because the programmer has pre-answered questions which would otherwise need to be disambiguated by a garbage collector at runtime. The less ambiguous behavior makes it faster to integrate modules, because the compiler can point out discrepancies between what the code owner said they expected and how something is being used by itself.
But it's not like Rust added that complexity - it was always there. C++, C#, and Java just let you ignore at the risk of adversely affecting software stability or performance, respectively.
> And who likes doing this to themselves anyway? Isn't it a very frustrating experience? How is this the most loved language?
The thing is, these dependencies do exist no matter what language you use if they stem from an underlying concept. In that case rust just makes you explicitly write them which is a good thing since in C++ all these dependencies would be more or less implicit and everytime somebody edits the code he needs to think all these cases through and get a mental model (if he sees it at all!). In Rust you at least have the lifetime annotations which make it A: obvious there is some special dependency going on and B: show the explicit lifetimes etc.
So what I'm saying, you need to put in this work no matter which language you choose, writing it down is then not a big problem anymore. If you don't think about these rules your program will probably work most of the time but only most of the time, and that can be very bad for certain scenarios.
> Rust does require structuring programs in a "Rust way" to avoid fighting with things it can't prove to be safe.
That was my concern that I had when I started learning the language. I think it is true, but I was surprised how quickly I managed to get used to the Rust way.
> It may be safer, but not as "ergonomic" subjectively, which ultimately affects productivity.
For my first large project in Rust, I ran into several issues where the type system was complaining that I couldn't take the references of certain variables in certain ways. Most of the time, it was clear to me (eventually) that what I wanted to do was actually unsafe. It takes some time, but a lot of the pain involved with starting out with Rust is realizing that the patterns you've been using all along in C/C++ were fundamentally unsafe from the beginning.
The rest of the time, though, what I wanted to do was safe, but I couldn't figure out how to structure the code to get Rust in a position where it could guarantee that safety.
> The problems I have while coding in Rust don't even exist in other languages.
Yes, Rust probably requires more thinking to program in than C. But C's hard-to-troubleshoot runtime segfaults become hard compiler errors (with line numbers and useful error messages!) in Rust. Trust me, I've had plenty of fights with the borrow checker. But, once I finally get it to compile successfully, I know that my program isn't going to fail at runtime by accessing memory in some invalid/incorrect way.
> The memory safety stuff is 90% writing safe code then fighting the compiler to agree with you.
This isn’t my experience, from a decade of occasionally watching and helping others learn Rust, personally and professionally. Rather, it’s normally writing code that you believe is safe, wrestling with the compiler and finally realising that it was right all along, or giving up before reaching this stage but it was still. There are certainly some cases where this isn’t the case, and this article talks about the most common and most notorious one, but seriously, the compiler is normally right.
Somewhere along the way, things click and your intuitions subsequently almost always match the compiler’s, and you have a much happier time of it, seldom needing to wrestle. And your coding in other languages tends to improve, or at least focus on ownership more in ways that tend to make later maintenance easier.
> Anyways, I guess this gets easier over time, right?
Yes.
> Should I avoid using closures all over the place?
Not necessarily.
> Should my code look more like C and less like Haskell?
Yes. Others sometimes don't like to hear this, but IMO, Rust is not at all functional. Passing functions around is not ergonomic (how many function types does Rust have again? Three?). Even making heavy use of Traits, especially generic ones, is difficult.
Rust is very much procedural. Java-style OOP doesn't work because of the borrowing/ownership. And FP style function composition doesn't work without Boxing everything. But then you'd need to be careful about reference cycles.
> Heck, the author praises Rust and it's memory safety, but then proceeds to criticise Rust's compiler because, as he puts it, the compiler will stop you from doing unsafe stuff again and again. Isn't that the whole point?
Rust ensures that all valid rust programs are safe, but this does not mean that programs that are difficult to express in rust are unsafe.
Consider how future improvements to rust may make some previously difficult to express patterns easier. I think non-lexical lifetimes would be one such example. You could imagine that someone who learned on a future version of rust might be similarly annoyed at compiler errors when working with an old version of rust that's more limited in what things can be expressed and understood as safe.
> we don’t know how to create a simpler memory safe low-level language.
This may well be true, but using a memory-safe language is never, ever the goal. The goal is creating correct and secure programs -- that have as few bugs and security flaws as possible/required -- as cheaply as possible. While a memory safe language in the style of Rust is one means toward that end, that eliminates an important class of bugs at the cost of language complexity, it is not the best way toward that goal, at least not that we know, and it is certainly not the only one [1]. I.e. the hypothesis that if I want to write a program that's as correct as possible/needed as cheaply as possible then I should necessarily use the language that gives me the most sound guarantees regardless of the cost this entails is just some people's guess. It's hard to say if it's a good guess or a bad one because it's clear that we're talking about specific sweet-spots on a wide spectrum of options that could be very context-dependent, but it's still a guess, with good arguments both in its favour as well as against.
> In Rust, there are choices to be made, some important enough to have dedicated syntax.
Not only that, but those choices are exposed in the type signature and are, therefore, viral. Changing some internal technical implementation detail can require changes in all consumers. This is not a problem with the type system -- on the contrary, Rust's type system ensures that all the changes that need to be made are made -- but it is a fundamental problem with all low level language. They all suffer from low abstraction, i.e. a certain interface can represent a smaller number of implementations than in high-level languages (even if the choice is not explicit in the type, like, say, in C, the usage pattern is part of the interface). But Rust's choice to expose such details in the types has its downsides as well.
> If you use C, you can use formal methods to prove the absence of undefined behaviors
C now also has sound static analysis tools [2] that guarantee no undefined behaviour with a nearly fully automatic proof that scales to virtually any code size and requires relatively little effort, certainly compared to a rewrite.
[1]: Another low-level language with an emphasis on the same goal of correctness, Zig, takes an approach that is radically different from Rust's and is so simple it can be fully learned in a day or two. Which of the two approaches, if any, is better for correctness can only be answered empirically.
> So if something I am saying doesn't seem to make sense, or seems "incorrect", well, maybe it's that I am just coming from a very different place in terms of what good programming looks like.
I do think this is probably true, and I know you do care about this! The thing is...
> The code that I write just looks way different from the code you guys write, the things I think about are way different, etc.
This is also probably true! The issue comes when you start describing how Rust code must be or work. There's nothing bad about having different ways of doing things! It's just that when you say things like "since then you are putting unsafe everywhere in the program," when that's empirically not what happens in Rust code.
> Using a bump allocator in the way you just did, on the stack for local code that uses the bump allocator right there, is semantically correct, but not a very useful usage pattern.
Yes. I thought going to the simplest possible thing would be best to illustrate the concept, but you're absolutely right that there is a rich wealth of options here.
Rust handles all four of these cases, in fairly straightforward ways. I also agree that 3 isn't often talked about as much as it should be in the broader programming world. I also have had this hunch that 3 and 4 are connected, given that the stack sometimes feels like an arena for just the function and its children in the call graph, and that it has some connection to the young generation in garbage collectors as well, but this is pretty offtopic so I'll leave it at that :)
Rust doesn't care just about 4 though! Lifetimes handle 3 as well; they ensure that the pointers don't last longer than the arena lives. That's it.
I don't have time to dig into this more, but I do appreciate you elaborating a bit here. It is very helpful to get closer to understanding what it is you're talking about, exactly. I think I see this differently than you, but I don't have a good quick handle on explaining exactly why. Some food for thought though. Thanks.
(Oh, and it is the one you're thinking of; I forgot that you had commented on that. My point was not to argue that the specifics were good, or that your response was good or bad, just that different strategies for handling memory isn't unusual in Rust world.)
> You can use rust as if it were a high level language without knowing anything about low level stuff.
How can you write that with a (I suppose) straight face? Rust makes you think about lifetime of every single variable, as anyone who's written a couple of lines of Rust knows... it' not just the lifetime annotations you will need when you get past the "copy everything" phase and start using or writing data structures, but the borrow checker making even the simplest stuff something you actually have to think through carefully (do you need to pass a reference, make a copy, use a mutable variation of some function, open a new block to limit the scope of the variable, assign it to a local variable to avoid it getting out of scope too early?). This is plainly "low level" stuff you need to worry about all the time which you just don't in any high level language.
> It often seems that every other post about Rust on HN says that if it compiles, it works.
I'm always careful not to say that "if it compiles, it works"--no language can guarantee that (well, except those that prove your program correct). Instead I like to say "if it compiles, it will fail for a not-stupid reason". :)
That is mostly only true for very early beginners.
There are certain patterns you have to learn and understand, especially for developers not accustomed to thinking about lifetimes ( which applies to most developers that only have experience with GC languages).
And sometimes you do have to battle the compiler, even with experience.
But most of that goes away pretty quickly once the language clicks for you.
After that Rust can still feel restrictive, but that's because there are very few languages that enforce as much correctness at compile time.
That very well can mean that Rust just isn't a good fit for certain domains - which is perfectly fine!
reply