It's like reading "A Discipline of Programming", by Dijkstra. That morality play approach was needed back then, because nobody knew how to think about this stuff.
Most explanations of ownership in Rust are far too wordy. See [1]. The core concepts are mostly there, but hidden under all the examples.
- Each data object in Rust has exactly one owner.
- Ownership can be transferred in ways that preserve the one-owner rule.
- If you need multiple ownership, the real owner has to be a reference-counted cell.
Those cells can be cloned (duplicated.)
- If the owner goes away, so do the things it owns.
- You can borrow access to a data object using a reference.
- There's a big distinction between owning and referencing.
- References can be passed around and stored, but cannot outlive the object.
(That would be a "dangling pointer" error).
- This is strictly enforced at compile time by the borrow checker.
That explains the model. Once that's understood, all the details can be tied back to those rules.
Maybe it's my learning limitations, but I find it hard to follow explanations like these. I had similar feelings about encapsulation explanations: it would say I can hide information without going into much detail. Why, from whom? How is it hiding if I can _see it on my screen_.
Similarly here, I can't understand for example _who_ is the owner. Is it a stack frame? Why would a stack frame want to move ownership to its callee, when by the nature of LIFO the callee stack will always be destroyed first, so there is no danger in hanging to it until callee returns. Is it for optimization, so that we can get rid of the object sooner? Could owner be something else than a stack frame?
Why can mutable reference be only handed out once? If I'm only using a single thread, one function is guaranteed to finish before the other starts, so what is the harm in handing mutable references to both? Just slap my hands when I'm actually using multiple threads.
Of course, there are reasons for all of these things and they probably are not even that hard to understand. Somehow, every time I want to get into Rust I start chasing these things and give up a bit later.
> Why would a stack frame want to move ownership to its callee
Rust's system of ownership and borrowing effectively lets you hand out "permissions" for data access. The owner gets the maximum permissions, including the ability to hand out references, which grant lesser permissions.
In some cases these permissions are useful for performance, yes. The owner has the permission to eagerly destroy something to instantly free up memory. It also has the permission to "move out" data, which allows you to avoid making unnecessary copies.
But it's useful for other reasons too. For example, threads don't follow a stack discipline; a callee is not guaranteed to terminate before the caller returns, so passing ownership of data sent to another thread is important for correctness.
And naturally, the ability to pass ownership to higher stack frames (from callee to caller) is also necessary for correctness.
In practice, people write functions that need the least permissions necessary. It's overwhelmingly common for callees to take references rather than taking ownership, because what they're doing just doesn't require ownership.
I think your comment has received excellent replies. However, no one has tackled your actual question so far:
> _who_ is the owner. Is it a stack frame?
I don’t think that it’s helpful to call a stack frame the owner in the sense of the borrow checker. If the owner was the stack frame, then why would it have to borrow objects to itself? The fact that the following code doesn’t compile seems to support that:
fn main() {
let a: String = "Hello".to_owned();
let b = a;
println!("{}", a); // error[E0382]: borrow of moved value: `a`
}
User lucozade’s comment has pointed out that the memory where the object lives is actually the thing that is being owned. So that can’t be the owner either.
So if neither a) the stack frame nor b) the memory where the object lives can be called the owner in the Rust sense, then what is?
Could the owner be the variable to which the owned chunk of memory is bound at a given point in time? In my mental model, yes. That would be consistent with all borrow checker semantics as I have understood them so far.
I believe this answer is correct. Ownership exists at the language level, not the machine level. Thinking of a part of the stack or a piece of memory as owning something isn’t correct. A language entity, like a variable, is what owns another object in rust. When that object goes at a scope, its resources are released, including all the things it owns.
> Why can mutable reference be only handed out once?
Here's a single-threaded program which would exhibit dangling pointers if Rust allowed handing out multiple references (mutable or otherwise) to data that's being mutated:
let mut v = Vec::new();
v.push(42);
// Address of first element: 0x6533c883fb10
println!("{:p}", &v[0]);
// Put something after v on the heap
// so it can't be grown in-place
let v2 = v.clone();
v.push(43);
v.push(44);
v.push(45);
// Exceed capacity and trigger reallocation
v.push(46);
// New address of first element: 0x6533c883fb50
println!("{:p}", &v[0]);
> Why would a stack frame want to move ownership to its callee, when by the nature of LIFO the callee stack will always be destroyed first, so there is no danger in hanging to it until callee returns.
It definitely takes some getting used to, but there's absolutely times when you could want something to move ownership into a called function, and extending it would be wrong.
An example would be if it represents something you can only do once, e.g. deleting a file. Once you've done it, you don't want to be able to do it again.
The owned memory may be on a stack frame or it may be heap memory. It could even be in the memory mapped binary.
> Why would a stack frame want to move ownership to its callee
Because it wants to hand full responsibility to some other part of the program. Let's say you have allocated some memory on the heap and handed a reference to a callee then the callee returned to you. Did they free the memory? Did they hand the reference to another thread? Did they hand the reference to a library where you have no access to the code? Because the answer to those questions will determine if you are safe to continue using the reference you have. Including, but not limited to, whether you are safe to free the memory.
If you hand ownership to the callee, you simply don't care about any of that because you can't use your reference to the object after the callee returns. And the compiler enforces that. Now the callee could, in theory give you back ownership of the same memory but, if it does, you know that it didn't destroy etc that data otherwise it couldn't give it you back. And, again, the compiler is enforcing all that.
> Why can mutable reference be only handed out once?
Let's say you have 2 references to arrays of some type T and you want to copy from one array to the other. Will it do what you expect? It probably will if they are distinct but what if they overlap? memcpy has this issue and "solves" it by making overlapped copies undefined. With a single mutable reference system, it's not possible to get that scenario because, if there were 2 overlapping references, you couldn't write to either of them. And if you could write to one, then the other has to be a reference (mutable or not) to some other object.
There are also optimisation opportunities if you know 2 objects are distinct. That's why C added the restrict keyword.
> If I'm only using a single thread
If you're just knocking up small scripts or whatever then a lot of this is overkill. But if you're writing libraries, large applications, multi-dev systems etc then you may be single threaded but who's confirming that for every piece of the system at all times? People are generally really rubbish at that sort of long range thinking. That's where these more automated approaches shine.
> hide information...Why, from whom?
The main reason is that you want to expose a specific contract to the rest of the system. It may be, for example, that you have to maintain invariants eg double entry book-keeping or that the sides of a square are the same length. Alternatively, you may want to specify a high level algorithm eg matrix inversion, but want it to work for lots of varieties of matrix implementation eg sparse, square. In these cases, you want your consumer to be able to use your objects, with a standard interface, without them knowing, or caring, about the detail. In other words you're hiding the implementation detail behind the interface.
That's not explaining ownership, that motivating it. Which is fine. The thing that's hard to explain and learn is how to read function signatures involving <'a, 'b>(...) -> &'a [&'b str] or whatever. And how to understand and fix the compiler errors in code calling such a function.
IME teaching students Rust, knowing C++ first actually is a detriment to learning because they have a bunch of C++ habits to unlearn. Those students "fight the borrow checker" much more than the blank slate students, because they have some idea about how code "should" be written.
I would say knowing the useless features of c++ is a pre-requisite for learning rust yes. It's yet another c++ replacement designed by a committee of phds.
You would think they would be smart enough to realize that a language taking X hours to learn is a language flaw not a user flaw, but modern education focuses on specialization talents rather than general intelligence.
The goal of some languages might be to be easy to learn. But most "system" languages focus on helping design good software, where "good" might mean reliable, maintainable or performant.
Writing good software most often is not easy. The learning curve of a particular language usually is only a modest part of what it takes.
Right but Rust is supposed to be a systems language. We already have dozens of languages that are easy to learn. The whole reason for having compile time memory management is to satisfy the constraints of a systems language...
I don't think it's much harder than learning C or C++ which are the only comparable mainstream languages.
I think it's actually easier, thanks to Cargo and the Crates ecosystem. Some of the hardest things for students are just building and linking code, especially third party libraries.
I run two intermediate programming courses, one where we teach C++, and another where we teach Rust. In the Rust course, by the first week they are writing code and using 3rd party libraries; whereas in the C course we spend a lot of time dealing with linker errors, include errors, segfaults, etc. The learning curve for C/C++ gets steep very fast. But with Rust it's actually quite flat until you have to get into borrowing, and you can even defer that understanding with clone().
By the end of the semester in the C++ course, students' final project is a file server, they can get there in 14 weeks.
In Rust the final project is a server that implements LSP, which also includes an interpreter for a language they design. The submissions for this project are usually much more robust than the submissions for the C++ course, and I would attribute this difference to the designs of the languages.
> Is it a lot different from std::unique_ptr in C++?
It’s both identical and very different, depending on the level of detail you want to get into. Conceptually, it’s identical. Strictly speaking, the implementations differ in a few key ways.
The good news is that idiomatically written good clean Rust code doesn't need to rely on such borrow signatures very often. That's more when you're leaving the norm and doing something "clever."
I know it throws people off, and the compiler error can be confusing, but actual explicit lifetimes as part of a signature are less common than you'd expect.
Summarizing a set of concepts in a way that feels correct and complete to someone who understands them, is a much easier task than explaining them to someone who doesn't. If we put this in front of someone who's only worked with call-by-sharing languages, do you think they'll get it right away? I'm skeptical.
For me it really clicked when I realized ownership / lifetimes / references are just words used to talk about when things get dropped. Maybe because I have a background in C so I'm used to manual memory management. Rust basically just calls 'free' for you the moment something goes out of scope.
All the jargon definitely distracted me from grasping that simple core concept.
Rust also has the “single mutable reference” rule. If you have a mutable reference to a variable, you can be sure nobody else has one at the same time. (And the value itself won’t be mutated).
Mechanically, every variable can be in one of 3 modes:
1. Directly editable (x = 5)
2. Have a single mutable reference (let y = &mut x)
3. Have an arbitrary number of immutable references (let y = &x; let z = &x).
The compiler can always tell which mode any particular variable is in, so it can prove you aren’t violating this constraint.
If you think in terms of C, the “single mutable reference” rule is rust’s way to make sure it can slap noalias on every variable in your program.
This is something that would be great to see in rust IDEs. Wherever my cursor is, it’d be nice to color code all variables in scope based on what mode they’re in at that point in time.
"Rust basically just calls 'free' for you the moment something goes out of scope."
C++ does that too with RAII. Go ahead and use whatever STL containers you like, emplace objects onto them, and everything will be safely single-owned with you never having to manually new or delete any of it.
The difference is that C++'s guarantees in this regard derive from a) a bunch of implementation magic that exists to hide the fact that those supposedly stack-allocated containers are in fact allocating heap objects behind your back, and b) you cooperating with the restrictions given in the API docs, agreeing not to hold pointers to the member objects or do weird things with casting. You can use scoped_ptr/unique_ptr but the whole time you'll be painfully aware of how it's been bolted onto the language later and whenever you want you can call get() on it for the "raw" underlying pointer and use it to shoot yourself in the foot.
Rust formalizes this protection and puts it into the compiler so that you're prevented from doing it "wrong".
> a bunch of implementation magic that exists to hide the fact that those supposedly stack-allocated containers are in fact allocating heap objects behind your back
The heap is but one source for allocator-backed memory. I've used pieces of stack for this, too. One could also use an entirely staticly sized and allocated array.
the tradeoff is that ~you have to guess where rust is doing the frees, and you might be wrong. in the end this would be strictly equivalent to an explicit instruction to free, with the compiler refusing to compile if the free location broke the rules.
Right. If you come to Rust from C++ and can write good C++ code, you see this as "oh, that's how to think about ownership". Because you have to have a mental model of ownership to get C/C++ code to work.
But if you come from Javascript or Python or Go, where all this is automated, it's very strange.
The list in the above comment isn’t a summary — it’s a precise definition. It can and must be carefully explained with lots of examples, contrasts with other languages, etc., but the precise definition itself must figure prominently, and examples and intuition should relate back to it transparently.
Practically, I think it suggests that learning the borrow checker should start with learning how memory works, rather than any concepts specific to Rust.
And, after someone who doesn't know rust reads this neat and nice summary, they would still know nothing about rust. (Except "this language's compiler must have some black magic in it.")
Ownership is easy, borrowing is easy, what makes the language super hard to learn is that functions must have signatures and uses that together prove that references don't outlive the object.
Also: it's better not store referenced object in a type unless it's really really needed as it makes the proof much much more complex.
This explanation doesn't expose anything meaningful to my mind, as it doesn't define ownership and borrowing, both words being apparently rooted in an analogy with financial asset management.
I'm not acquainted with Rust, so I don't really know, but I wonder if the wording plays a role in the difficulty of concept acquisition here. Analogies are often double edged tools.
Maybe sticking to a more straight memory related vocabulary as an alternative presentation perspective might help?
The way I think about it is more or less in terms of how a C program would work: if you assume a heap allocated data structure, the owner is the piece of code that is responsible for freeing the allocation at the appropriate time. And a reference is just a pointer with some extra compile time metadata that lets the borrow checker prove that the reference doesn’t outlive the referent and that there’s no mutable aliasing.
If you've worked inside of CPython or other programs with manual reference counting, the idea of borrowing shows up there, where you receive a reference from another part of the program and then mess with the object without tweaking the reference count, "borrowing" an existing reference because any copies you've of the address will be short lived. The term shows up throughout CPython.
I find it strange that you relate borrowing and ownership to financial asset management.
From that angle, it indeed doesn’t seem to make sense.
I think, but might be completely wrong, that viewing these actions from their usual meaning is more helpful: you own a toy, it’s yours to do as tou please. You borrow a toy, it’s not yours, you can’t do whatever you want with it, so you can’t hold on to it if the owner doesn’t allow it, and you can’t modify it for the same reasons.
1. In real life I can borrow a toy from you and while I have that toy in my hands, the owner can exchange ownership with somebody else, while the object is borrowed by me. I.e. in real life the borrowing is orthogonal to ownership. In rust you can't do that.
2. Borrowing a toy is more akin to how mutable references work in rust. Immutable references allow multiple people to play with the same toy simultaneously, provided they don't change it.
What do you mean with usual sense? Maybe it's "financial" that put the interpretation out of the track, but financial comes fidus, that is trust, as in trust that outcomes of reality will meet some expectation of a mental representation.¹
"You own a toy" is the first thing a child is teached as wrong assumption by reality if not by careful social education, isn't it? The reality is, "you can play with the toy in some time frame, and sharing with others is the only way we can all benefit of joyful ludic moment, while claims of indefinite exclusive use of the toy despite limited attention span that an individual can spend on it is socially detrimental."
Also memory as an abstract object pragmatically operate on very different ground than a toy. If we could duplicate any human hand grabbable object as information carried by memory holding object, then any economy would virtually be a waste of human attention.
¹ edit: actually I was wrong here, I have been in confusion with "fiduciary". Finance instead comes from french "fin"(end), as in "end of debt".
Many people can borrow your toy to have look at it, but only one person can borrow it and play with it. And they are only allowed to play while no one is watching. And if you want to modify your toy with some tool it's not your's anymore, it yas moved and now belongs to the tool.
I guess I'm trying to say that analogy is of limited use here.
On the most abstract level, even "I" and "think" are misleading notions of what’s passing through current attention. So "borrowing" and "owning" are not really great starting point notions to "think" in that sense. But on the more mundane level of mentally handling stuffs, that’s an analogy that can have its own merits (and flaws, of course).
That really doesn't explain the model because you have completely left out the distinction between exclusive/shared (or mutable/immutable) borrows. Rust made a large number of choices with respect to how it permits such borrows and those do not follow from this brief outline nor from intuition or common sense. For example, the no aliasing rule is motivated not by intuition or common sense but from a desire to optimize functions.
The most complicated aspect of the borrows comes about from the elision rules which will silently do the wrong thing and will work fantastically until they don't at which point the compiler error is pointing at a function complaining about a lifetime parameter of a parameter with the trait method implying that the parameter has to live too long but the real problem was a lifetime in the underlying struct or a previous broken lifetime bound. Those elision rules are again not-intuitive and don't fall out of your explanation axiomatically. They were decisions that were made to attempt to simplify the life of programmers.
I often wanted to find writings about the 60s on how they approached system/application state at assembly level. I know Sutherland Sketchpad thesis has a lot of details about data structures but I never read it (except for 2-3 pages).
The second bullet in the second section is overpromising badly. In fact there are many, many, many ways to write verifiably correct code that leaves no dangling pointers yet won't compile with rustc.
Frankly most of the complexity you're complaining about stems from attempts to specify exactly what magic the borrow checker can prove correct and which incantations it can't.
A great teaching technique I learned from a very good match teacher is that when explaining core concepts, the simplified definitions don't need to be completely right. They are much simpler to grasp and adding exceptions to these is also quite easy compared to trying to understand correct, but complex, definitions at the beginning.
Yeah, but the whole purpose here is "flattening the learning curve", and telling people code will work when it won't is doing the opposite.
That bullet, at its most charitable, defines the "idealized goal" of the borrow collector. The actual device is much less capable (as it must be, as the goal is formally undecidable!), and "learning rust" requires understanding how.
> ... defines the "idealized goal" of the borrow collector. The actual device is much less capable
I think here you expanded on the original point in a good way. I would then continue with adding additional set of points covering the issue in greater detail and a set of examples of where this commonly happens and how to solve it.
Ironically, most people understand "learning curves" counterintuitively.
If a "learning curve" is a simple X-Y graph with "time" and "knowledge" being on each axis respectively, then what sort of learning curve is preferable: a flatter one or a steep one?
Clearly, if you graph large increases of knowledge over shorter periods of time, a steeper learning curve is more preferable. "Flattening the learning curve" makes it worse!
But for some reason, people always reverse this meaning, and so the common idiom breaks down for people who try to reason it out.
Replace "knowledge" with "required knowledge". It's not about how efficiently you can learn, but how much do you need to learn in a specific amount of time. If you need to learn a lot in short amount of time (which is a hard thing to do) the curve is steep. You can flatten the curve by increasing the time you have available or by requiring less knowledge.
As a systems programmer I found Rust relatively easy to learn, and wonder if the problem is non-systems programmers trying to learn their first systems language and having it explicitly tell them "no, that's dangerous. no, that doesn't make sense". If you ask a front end developer to suddenly start writing C they are going to create memory leaks, create undefined behavior, create pointers to garbage, run off the end of an array, etc. But they might "feel" like they are doing great because there program compiles and sort of runs.
If you have already gotten to the journeyman or mastery experience level with C or C++ Rust is going to be easy to learn (it was for me). The concepts are simply being made explicit rather than implicit (ownership, lifetimes, traits instead of vtables, etc).
It took me a few tries to get comfortable with Rust—its ownership model, lifetimes, and pervasive use of enums and pattern matching were daunting at first. In my initial attempt, I felt overwhelmed very early on. The second time, I was too dogmatic, reading the book line by line from the very first chapter, and eventually lost patience. By then, however, I had come to understand that Rust would help me learn programming and software design on a deeper level. On my third try, I finally found success; I began rewriting my small programs and scripts using the rudimentary understanding I had gained from my previous encounters. I filled in the gaps as needed—learning idiomatic error handling, using types to express data, and harnessing pattern matching, among other techniques.
After all this ordeal, I can confidently say that learning Rust was one of the best decisions I’ve made in my programming career. Declaring types, structs, and enums beforehand, then writing functions to work with immutable data and pattern matching, has become the approach I apply even when coding in other languages.
I had quite a similar experience. During the 3rd attempt at learning, everything seemed to click and I was able to be effective at writing a few programs.
This is all despite a long career as a programmer. Seems like some things just take repetition.
The "Dagger" dependency injection framework for the JVM took me 3 'learning attempts' to understand as well. May say more about myself than about learning something somewhat complicated.
Your experience matches an observation I have made, that when C++ developers approach Rust for the first time they often "fight the borrow checker" when they use C++ idioms in Rust. Then they start to learn Rust idioms, and bring them back to C++, which causes them to write more robust code despite not having and borrow checking at all.
But it is true. My own biggest mistake when learning Rust was that I tried to torce Object Oriented paradigms on it. That went.. poorly. As soon as I went "fuck it, I just do it like you want" things went smoothly.
>Your programming language shouldn't constrict you in those ways
Says who? Programming languages come in all shapes and sizes, and each has their tradeoffs. Rust's tradeoff is that the compiler is very opinionated about what constitutes a valid program. But in turn it provides comparable performance to C/C++ without many of the same bugs/security vulnerabilities.
Every programming language has constrictions by the nature of having syntax.
In JavaScript you can declare a variable, set it to 5 (number), and then set it to the "hello" (string), but that's not allowed in e.g. C. Is C constricting me too much because I have to do it in C's way?
Abusive relationships involve coercion, control, fear, and often violate personal autonomy and consent. One party dominates the other in a harmful way. Using Rust is not harmful.
Placing restrictions on the programs a programmer can write is not abusive. The rules exist to ensure clarity, safety, performance, and design goals. In an abusive relationship, rules are created to control or punish behavior, often changing capriciously and without reason or consultation. By contrast, Rust is designed by a group of people who work together to advance the language according to a set of articulated goals. The rules are clear and do not change capriciously.
Abuse causes emotional trauma, isolation, and long-term harm. Rust may cause feelings of frustration and annoyance, it may make you a less efficient programmer, but using it does not cause psychological or physical harm found in abusive relationships.
- it's very different from other languages. That's intentional but also an obstacle.
- it's a very complex language with a very terse syntax that looks like people are typing with their elbows and are hitting random keys. A single character can completely change the meaning of a thing. And it doesn't help that a lot of this syntax deeply nested.
- a lot of its features are hard to understand without deeper understanding of the theory behind them. This adds to the complexity. The type system and the borrowing mechanism are good examples. Unless you are a type system nerd a lot of that is just gobblygook to the average Python or Javascript user. This also makes it a very inappropriate language for people that don't have a master degree in computer science. Which these days is most programmers.
- it has widely used macros that obfuscate a lot of things that further adds to the complexity. If you don't know the macro definitions, it just becomes harder to understand what is going on. All languages with macros suffer from this to some degree.
I think LLMs can help a lot here these days. When I last tried to wrap my head around Rust that wasn't an option yet. I might have another go at it at some time. But it's not a priority for me currently. But llms have definitely lowered the barrier for me to try new stuff. I definitely see the value of a language like users. But it doesn't really solve a problem I have with the languages I do use (kotlin, python, typescript, etc.). I've used most popular languages at some point in my life. Rust is unique in how difficult it is to learn.
>it's very different from other languages. That's intentional but also an obstacle.
It's very different from a lot of the languages that people are typically using, but all the big features and syntax came from somewhere else. See:
>The type system and the borrowing mechanism are good examples. Unless you are a type system nerd a lot of that is just gobblygook to the average Python or Javascript user.
Well, yeah, but they generally don't like types at all. You won't have much knowledge to draw on if that's all you've ever done, unless you're learning another language in the same space with the same problems.
Macros are introduced early in Rust for a couple reasons.
1. println!() is a macro, so if you want to print anything out you need to grapple with what that ! means, and why println needs to be a macro in Rust.
2. Macros are important in Rust, they're not a small or ancillary feature. They put a lot of work into the macro system, and all Rust devs should aspire to use and understand metaprogramming. It's not a language feature reserved for the upper echelon of internal Rust devs, but a feature everyone should get used to and use.
Hard disagree, macros almost always lead the "too clever for you own good" even when the macro system is safe. Macros should always be used sparingly, and I think that Rust teeters on the edge of encouraging too much complexity for the sake of convenience. As a systems programmer I am allergic to unnecessary levels of indirection that make it more difficult for me to reason about what the system is actually doing and how performance and hardware interaction will be affected.
I don't know how to read your comment other than "nothing hard is worth doing". Some things have benefits and drawbacks, is the existence of drawbacks always a non-starter for you?
I'm trying to phrase this as delicately as I can but I am really puzzled.
If someone wrote an article about how playing the harp is difficult, just stick with it... would you also say that playing the harp is a terrible hobby?
Maybe people need persuading to learn Rust not just because they think it's hard, but also because they think it's bad? Not everything hard is worth doing. Difficulty is just one of the factors to consider.
I started to learn Rust, but I was put off by the heavy restrictions the language imposes and the attitude that this is the only safe way. There's a lack of acknowledgement, at least in beginner materials, that by choosing to write safe Rust you're sacrificing many perfectly good patterns that the compiler can't understand in exchange for safety. Eventually I decided to stop because I didn't like that tradeoff (and I didn't need it for my job or anything)
The thing is, if you want to learn Rust this site contains good advice on how to do it. I know, because I learned Rust.
Rust isn't a language you should pick up if you're not ready to put in the work. Just like you shouldn't go for full blown automotive grade C coding if you just want to learn coding quickly to get a job or something.
Rust has a steep learning curve, but the harder part (as mentioned in the article) is to unlearn patterns from other programming languages if you think you're already a good programmer.
I think one can understand Rust and still dislike it? Not every criticism of Rust comes from thinking it is too hard. I appreciate it for what it is and the problems it tries to solve. I just don't like many aspects of design of the language, seeing it as unnecessarily ugly for achieving it's aims.
> by choosing to write safe Rust you're sacrificing many perfectly good patterns that the compiler can't understand in exchange for safety
Historically, programmers drastically overestimate their ability to write perfectly safe code, so it's an enormous benefit if the compiler is able to understand whether it's actually safe.
The first part of your statement feels true, although that's... unverified and lacks actual backing up.
The second part of your statement is very debatable based on what safe means in this case, and whether it's an enormous benefit for a given situation.
There's plenty of stories [0][1] about Rust getting in the way and being very innappropriate for certain tasks and goals, and those "enormous benefits" can become "enormous roadblocks" in different perspectives and use cases.
In my personal and very subjective opinion I think Rust can be very good when applied to security applications, realtime with critical safety requirements (in some embedded scenarios for example), that sort of stuff. I think it really gets in the way too much in other scenarios with demanding rules and pattern that prevent from experimenting easily and exploring solutions quickly.
If it takes the average person 1 million hours to learn rust then the average person won't learn rust
If it takes the average person 1 hour to learn rust then the average person will learn rust.
If you were designing a language which would you pick all else being equal?
To your question, no but I wouldn't be puzzled when most people pick up a guitar. (Both are so much more intuitive than any programming language so the metaphor sets false expectations. Slick political move, but probably just turns more people off of Rust)
> If you were designing a language which would you pick all else being equal?
But why would you think all else is equal? You might not agree with the tradeoffs Rust makes, and it's not as if there's a perfect language for all uses, but it absolutely makes hard software easier to write.
I've had the opportunity to debug weird crazy memory corruption, as well as "wow it's hard to figure out how to design this in Rust", and having come to terms with things much like this blog post I now get more work done, with less corruption _and_ design problems.
The truth is that by the time you are a senior developer, you will have encountered the lessons that make rust worth learning but may not have truly understood all the implications.
Many people will think, I have a garbage collected language, rust has nothing to teach me. Even in garbage collected languages, people create immutable types because the possibility of shared references with mutability makes things incredibly chaotic that they look for immutability as a sort panacea. However, once you have immutable types you quickly realize that you also need ergonomic ways of modifying those objects, the methods you create to do so are often more cumbersome than what would be permitted for a mutable object. You wish there was some way to express, "There is a time where this object is mutable and then it becomes immutable." Enter the borrow checker.
Once you are borrow checking... why are you garbage collecting? Well, expressing those timelines of mutability and existence is a cost because you need to understand the timeline and most people would rather not spend that energy--maybe mutability or the poor ergonomics of immutable objects wasn't so bad. So, I garbage collect because I do not want to understand the lifetimes of my objects. Not understanding the lifetimes of objects is what makes shared mutability hard. Immutability eliminates that problem without requiring me to understand. Rust can teach this lesson to you so that you make an informed choice.
Of course, you can also just listen to me and learn the same lesson but there is value for many people to experience it.
Rust design decisions are pretty hard to understand sometimes, Mojo is another language with a borrow-checker but it is not nearly as hard to learn as Rust due to making a few decisions.
First is value semantics, in Rust people are told to always clone when learning, why isn't this semantics built into the language? It is what you have in most static languages - C, C++, Go, etc. This is the mental model many people come to Rust with.
Secondary, Mojo's lifetime does not tell the compiler when a value is safe to use but when it is safe to delete, in this way the lifetime is not scope based, references will extend the lifetime of the value they reference, but values will be destroyed immediately after their last use. In Mojo you'll never see "value does not live long enough".
Just these two design decisions defines away so many ergonomic issues.
> people are told to always clone when learning, why isn't this semantics built into the language?
Because cloning as opposed to copying is expensive and it generates a new instance of a type.
In C, you don't clone, you simply copy the struct or pointer, which will lead to a pointer to the same memory or a struct with members pointing to the same memory.
C++ on the other hand has a copy constructor, and you have to move explicitly, often generating unnecessary copies (in the sense of clone)
> Mojo's lifetime does not tell the compiler when a value is safe to use but when it is safe to delete,
What happens if you pass the variable mutably to a function?
> What happens if you pass the variable mutably to a function?
What happens in what manner? Mojo uses ASAP memory model, values will always be destroyed at the point of its last use. Mojo dataflow analysis will track this.
In terms of safety, Mojo will enforce `alias xor mutability` - like in Rust.
> C++ on the other hand has a copy constructor, and you have to move explicitly, often generating unnecessary copies (in the sense of clone)
Mojo also has copy and move constructors, but unlike in C++ these are not synthesised by default; the type creator has to either explicitly define the constructors or add a synthesiser. In Mojo, you can have types that are not copyable and not movable, these types can only be passed by reference. You can also have types that are copyable but not movable, or movable but not copyable.
Learning any programming language at all feels 10x as hard to beginners, so you might as well say programming is not worth learning period in this case. Anything new has a learning curve to it.
The article focuses on the learning curve rather than the problem Rust is solving, as an observation. Think you need both of those to draw a conclusion as to whether it’s worth doing.
I think you can have a lot of debate on the design decisions on Rust, but I don't think the need for these articles tell you a lot about the language itself. I'd argue that Python needs articles like this more so than Rust does, but for entirely different reasons. In two decades of more and more programmers who aren't coming from an engineering background, I've yet to see anyone who used a Python generator or slots. Data Classes are less rare, but mainly in the form of pydantics "version". Which doesn't exactly matter for a lot of Python code... This is a world where 4chan can serve 4 million concurrent users an apache server running a 10k line PHP file neither of which have been updated since 2015... so you can be fine doing inefficient and entirely in-memory Python code 95% (or more) of the time.
That doesn't mean you should though. Imagine how much energy is being wasted globally on bad Python code... The difference is of course that anyone can write it, and not everyone can write Rust. I'm not personally a big fan of Rust, I'd chose Zig any day of the week... but then I'd also choose C over C++, and I frankly do when I optimise Python code that falls in those last 5%. From that perspective... of someone who really has to understand how Python works under the hood and when to do what, I'd argue that Rust is a much easier langauge to learn with a lot less "design smell". I suppose Python isn't the greatest example as even those of us who love it know that it's a horrible language. But I think it has quite clearly become the language of "everyone" and even more so in the age of LLM. Since our AI friends will not write optimised Python unless you specifically tell them to use things like generators and where to use them, and since you (not you personally) won't because you've never heard about a generator before, then our AI overlords won't actually help.
As someone who learned Rust, bur mostly uses Python in the day to day, I don't think Rust has a language design smell. It is just a very strict language with some of the strictness out there to ruin your day if you try to program Rust like it isn't Rust.
What that means is for example, if you have high aesthetical ideals and try to write object oriented code you will hit a brick wall eventually. Why? Notnbecause Ruwt is a bad language, but because you try to write Rust like it is Java or something.
Rust is a very nice language if you respect that there are Rust-ways of doing things that and that these ways are more data oriented than you might be used to.
The strictness can be daunting for beginners, but with increasing complexity it becones an absolute godsend. Where in other languages I find errors only when they happen, most Rust code just works (provided you write it in a Rust way), because the errors will caught during compilation.
That doesn't prevent logic errors, but these can be addressed with the absolute stellar test integrations. Now Rust is not all roses, but it is certainly a language worth learning even if you never use it. The ways it mitigates certain classes of errors can be turned into good coding practises for other languages as well.
There _are_ more than two programming languages, though. I feel like most of the debates about Rust devolve into the same false choice between safety and ease.
Before Rust I was hearing the same argument from Haskell or Scala developers trying to justify their language of choice.
I know Rust is here to stay, but I think it’s mostly because it has a viable ecosystem and quality developer tools. Its popularity is _in spite of_ many of its language features that trade that extra 1% of safety for 90% extra learning curve.
> features that trade that extra 1% of safety for 90% extra learning curve.
I remember both MS and goog having talks about real-world safety issues in the range of 50% of cases were caused by things that safe rust doesn't allow (use after free, dangling pointers, double free, etc). The fact that even goog uses it, while also developing go (another great language with great practical applications) is telling imo.
I have taken the time to learn rust and you're absolutely right. It's a very complex, design-by-committee language. It has brilliant tooling, and is still much less complex than it's design-by-committee competitor C++, but it will never be easy to learn.
I find it relatively simple. Much simpler than C++ (obviously). For someone who can write C++ and has some experience wth OCaml/Haskell/F#, it's not a hard language.
Sure, C++ has a more complex spec, nobody can argue against that.
Complex is the wrong word. Baffling is a better word. Or counterintuitive, or cumbersome. If “easy enough for someone with experience in C++, OCaml, Haskell, and F#” were the same thing as “not hard” then I don’t think this debate would come up so frequently.
What you call "baffling", I call "different". Being different doesn't mean it's "complex" or even "hard" (in isolation), but it can be baffling, in the same way that driving on the other side of the road for the first time can feel baffling (but doesn't mean it's "wrong").
Of course, this is very subjective. For someone who only knows python or javascript at a superficial level, Rust may seem out of reach. But if you're ok with the most common programming paradigms, I don't find Rust baffling.
I mean, you can't expect to learn a new language in a few days, it'll always take a bit of work. My feeling is that people complaining of the language being hard aren't putting the effort.
My experience is that Rust is a relatively small language which doesn't introduce a lot of new concepts. The syntax is quite intuitive, and the compiler super helpful. The borrower checker was the only new thing for me. I'm not an expert at all, but my experience is that after spending 2 weeks full-time reading books and experimenting, I was able to work professionally with the language without feeling too much friction.
On the other hand, after spending much more time on C++, I don't feel really comfortable with the language.
C++ is a huge and complex language. I worked in it, on and off, from 2002 through 2014 or so and never really felt comfortable, either. Everyone seems to use their own dialect.
Well, it does look like there is a will to mimic religious social structure in the community, be it as a satiric form of it. I mean, I guess they purposefully named their pancakes cargo, as in "cargo cult", didn't they? Rustacean, rustomicon, and the other few words I saw leak out of the community all seem to go in the same spirit. I'm almost surprised they didn't went with more fancy terms for these core concepts of ownership and borrowing. Perl was also full of religious stuff like blessing your object, though Larry was actually more in the "true devot" side of the line.
the dogmatic culture would probably be my first suggestion. i always ask why are there any CVEs for rust if its "memory-safe" but never get an answer suprisingly
> i always ask why are there any CVEs for rust if its "memory-safe" but never get an answer suprisingly
The answer is straightforward: bugs exist. Even in formally proven software, mistakes can be made. Nothing is perfect.
Additionally, memory safety is a property that when people talk about it, they mean by default. All languages contain some amount of non-proven unsafe code in their implementation, or via features like FFI. Issues can arise when these two worlds interact. Yet, real-world usage shows that these cases are quite few compared to languages without these defaults. The exceptions are also a source of the CVEs you’re talking about.
CVE is not only for memory leak though, while eliminating (or even drastically reducing) such a class of issue is a fair point to advertise, it should not be confused as a magic safety facility that makes go away any security concern.
its not design by committee its design by Pull request
It doesn't have a central https://en.wikipedia.org/wiki/Benevolent_dictator_for_life like python used to
so people suggest and implement features as a group,
with code counting for a lot (although theoretical issues with safety/design also matter) as opposed to companies arguing for their pet features endlessly without much difference. Look at how long it takes C++ to get any new features.
> If a language needs an article like this, absolutely begging people to bite the bullet to learn it, maybe that's a language design smell.
The problem with articles like this is that they don't really get to the heart of the problem:
There are programs that Rust will simply not let you write.
Rust has good reasons for this. However, this is fundamentally different from practically every programming language that people have likely used before where you can write the most egregious glop and get it to compile and sometimes even kinda-sorta run. You, as a programmer, have to make peace with not being able to write certain types of programs, or Rust is not your huckleberry.
Turning completeness doesn’t take efficiency into account, nor the reality of things like “call into the operating system so that you can display output” that are necessary when building real systems.
> There are programs that Rust will simply not let you write.
If you're writing purely safe code, I will say this is true in a practical sense, but you can almost always use unsafe to write whatever you think rust won't let you do.
I'm not sure there are many cases where I would choose rust. I'm open to it. I just think in any given situation there would most likely be a better option.
Perhaps it will become prevalent enough that it will make sense in the future.
As systems programmer there are no better options. C is inherently unsafe, C++ is awful to work with and unsafe without careful use of pre-made safe abstractions that can't catch everything at runtime...
I have written C for decades and love the language, but Rust has convinced me that we need to evolve beyond the 70s. There's no excuse anymore.
It's the best fit I'm aware of for any task where correctness is worth the dev time hit. That includes systems programming tasks, where you're shuffling memory and files and system resources around and an error could kill your program or corrupt resources; or as a library for critical parts of your program, which could then provide an interface for other languages to use that allow for faster development & prototyping.
It's definitely the perfect language for writing a browser from scratch, as it was designed for almost 20 years ago. Of course nowadays it's already completely dominating that area, and their creator's "unix" has taken over the world and was not overtaken by something a random dude called ladybird.
"Safe" Rust is generally a simple language compared to C++. The borrow checker rules are clean and consistent. However, writing in it isn't as simple or intuitive as what we've seen in decades of popular systems languages. If your data structures have clear dependencies—like an acyclic graph then there's no problem. But writing performant self-referential data structures, for example, is far from easy compared to C++, C, Zig, etc.
On the opposite, "Unsafe" Rust is not simple at all, but without it, we can't write many programs. It's comparable to C, maybe even worse in some ways. It's easy to break rules (aliasing for exmaple). Raw pointer manipulation is less ergonomic than in C, C++, Zig, or Go. But raw pointers are one of the most important concepts in CS. This part is very important for learning; we can't just close our eyes to it.
And I'm not even talking about Rust's open problems, such as: thread_local (still questionable), custom allocators (still nightly), Polonius (nightly, hope it succeeds), panic handling (not acceptable in kernel-level code), and "pin", which seems like a workaround (hack) for async and self-referential issues caused by a lack of proper language design early on — many learners struggle with it.
Rust is a good language, no doubt. But it feels like a temporary step. The learning curve heavily depends on the kind of task you're trying to solve. Some things are super easy and straightforward, while others are very hard, and the eventual solutions are not as simple, intuitive or understandable compared to, for example, C++, C, Zig, etc.
Languages like Mojo, Carbon (I hope it succeeds), and maybe Zig (not sure yet) are learning from Rust and other languages. One of them might become the next major general-purpose systems language for the coming decades with a much more pleasant learning curve.
One approach that I don't see often enough is to focus on learning a subset of the language first. For example, in my own book on Rust, I skip teaching lifetimes. It's not necessary to write functions with lifetimes to build quite a few fully functioning programs. The same with macros (although the fact that their signatures are opaque doesn't make it easy for beginners). On the other hand, I disagree with the advice to rely on copy() or clone() - it's better to learn about borrowing from the beginning since it's such a fundamental part of the language.
These replies mainly revealed to me the general response people have on being corrected. Its very easy to become a stubborn programmer after being in the industry for so long.
I'd advise these people to personally figure out why they're so against compiler suggestions. Do you want to do things differently? What part stops you from doing that?
Borrow checker wouldn't get off my damn case - errors after errors - so I gave in. I allowed it to teach me - compile error by compile error - the proper way to do a threadsafe shared-memory ringbuffer. I was convinced I knew. I didn't. C and C++ lack ownership semantics so their compilers can't coach you.
Everyone should learn Rust. You never know what you'll discover about yourself.
It's an abstraction and convenience to avoid fiddling with registers and memory and that at the lowest level.
Everyone might enjoy their computation platform of their choice in their own way. No need to require one way nor another. You might feel all fired up about a particular high level language that you think abstracts and deploys in a way you think is right. Not everyone does.
You don't need a programming language to discover yourself. If you become fixated on a particular language or paradigm then there is a good chance you have lost sight of how to deal with what needs dealing with.
You are simply stroking your tools, instead of using them properly.
@gerdesj your tone was unnecessarily rude and mean. Part of your message makes a valid point but it is hampered by unnecessary insults. I hope the rest of your day improves from here.
I don’t specifically like Rust itself. And one doesn’t need a programming language to discover themselves.
My experience learning Rust has been that it imposes enough constraints to teach me important lessons about correctness. Lots of people can learn more about correctness!
I’ll concede- “everyone” was too strong; I erred on the side of overly provocative.
It does not teach you any fundamental lessons about correctness. It teaches you lessons about correctness within the framework Rust imposes; that's all
I think most of us enamoured with rust are c++ refugees glad the pain is lessened. The tooling including the compiler errors really are great though. I like the simplicity of c, but I would still pick rust for any new project just for the crates and knowing I'll never have to debug a segfault. I like pytorch and matlab fine for prototyping. Not much use for in-between languages like go or c# but I like the ergonomics of them just fine. I don't think it is at all weird for people coming from c++ or even c to like rust and prefer it over those other languages. We have already paid the cost of admission, and it comes with real benefits.
> You could see similar behavioural issues with C++ back in the days
I think that it's happened to some degree for almost every computer programming language for a whiles now - first was the C guys enamoured with their NOT Pascal/Fortran/ASM, then came the C++ guys, then Java, Perl, PHP, Python, Ruby, Javascript/Node, Go, and now Rust.
The vibe coding people seem to be the ones that are usurping Rust's fan boi noise at the moment - every other blog is telling people how great the tool is, or how terrible it is.
I know this feels like a positive vibe post and I don’t want to yuck anyone’s yum, but speaking for myself when someone tells me “everyone should” do anything, alarm bells sound off in my mind, especially when it comes to programming languages.
Yeah I agree, I enjoy the process. I don’t think that’s what’s behind “everyone should learn rust” in this case, and many cases. It feels like a “cause”.
The compilers maybe not, but static analysers already go a long way, it is a pity that it is still a quixotic battle to make developers adopt them, even if it isn't 100% all the way there.
If it isn't the always hated SecDevOps group of people pushing for the security tooling developers don't care about, at very least on build pipelines, they would keep collecting digital dust.
The only way I think I’ll learn rust is if there’s a surge of job opportunities paying 300 K and up which require it.
The potential is definitely there, it looks like it might compete with C++ in the quant .
But we already have ocaml . From Jane Street, at least for me if you’re going to tell me, it’s time to learn an extremely difficult programming language, I need to see the money.
So far my highest paid programming job was in Python
My problem with rust is not the learning curve, but the absolute ugliness of the syntax. It's like Perl and C++ template metaprogramming had a child. I just can't stand it.
Python is my favourite, C is elegance in simplicity and Go is tolerable.
C may be simple, but its too simple to be called elegant. The lack of namespacing comes to mind. Or that it is a staticly typed language, whose type system is barely enforced (you have to declare all types, but sometimes it feels like everything decays to int and *void without the right compiler incantations). Or the build system, where you have to learn a separate language to generate a separate language to compile the program (which a both also not really simple and elegant in my eyes). Or null-terminated strings: to save some 7 bytes per string (on modern platforms) C uses one of the most dangerous and unelegant constructs in the popular part of the programming-world. Or the absolutely inelegant error handling, where you either return an in-band-error-value, set a global variable or both or just silently fail. Or the standard-library, that is littered with dangerous functions. Or the reliance of the language definition on undefined behaviour, that forces you to read a 700-page, expensive document back to back to know whether a vital check in your program might be ignored by compilers or when your program might shred your hard drive, despite you never instructing it to do so. Or...
C has a simple syntax, but it is most certainly not elegant.
C is elegant because as an extremely powerful programming language used to create an uncountable number of high-profile projects it's simple enough that I feel optimistic I could write a C compiler myself if it was really necessary.
It may be impractical for some tasks but the power:complexity rate is very impressive. Lua feels similar in that regard.
The mechanism for sharing definitions, 'include' aka "copy file here", is absolutely not elegant. The absolute minimum you could do maybe, but not elegant.
Not to mention the whacky preprocessor as a whole...
I would say includes/standard library/compilers going crazy on UB is part of the infrastructure or ecosystem around the language and not the language itself. And I agree absolutely, while C the language is beautiful, the infra around it is atrocious.
Python community famously learned the hard way that sometimes the programmer needs to know that there are multiple kinds of string.
Personally, I’ve been using to_owned instead. Some of the people looking at my code don’t write rust, and I figure it makes things a bit easier to understand.
I’m not sure why it’s counterintuitive that &str and String are different things. Do you also find it counterintuitive in C++ that std::string is different from const char* ? What about &[u8] and Vec<u8> ?
Strings are like time objects: most people and languages only ever deal with simplified versions of them that skip a lot of edge cases around how they work.
Unfortunately going from most languages to Rust forces you to speedrun this transition.
C++ is a horribly cmplicated language, sometimes I have to cast something to an int when it's already an integer. /s
I have a hard time understanding why people have such a hard time accepting that you need to convert between different text representations when it's perfectly accepted for numbers.
You may be able to draw one that way but it completely neglects the way people use the term ordinarily “a steep learning curve” is not an easy to learn thing.
In point of fact, I think the intended chart of the idiom is effort (y axis) to reach a given degree of mastery (x axis)
I don't think the idiom has in mind any particular curve. I think it's just another case of a misuse becoming idiomatic without any meaning beyond the phrase taken as a unit. E.g.
- another think coming -> another thing coming
- couldn't care less -> could care less
- the proof of the pudding is in the eating -> the proof is in the pudding
It's usually not useful to try to determine the meaning of the phrases on the right because they don't have any. What does it mean for proof to be in a pudding for example?
The idiom itself is fine, it's just a black box that compares learning something hard to climbing a mountain. But learning curves are real things that are still used daily so I just thought it was funny to talk as if a flat one was desirable.
Calling it inaccurate was too harsh; my definition only became common usage in 1970, and the original “time vs learning” is still used in academic circles.
Ownership and lifetimes are arguably not about Rust, but about how we construct programs today. Big, interdependent, object graphs are not a good way of constructing programs in Rust or C++.
"You will have a much better time if you re-read your code to fix stupid typos before pressing “compile.”"
This is a strange one - I thought the rust compiler had famously helpful error messages, so why would I want to pore over my code looking for stupid typos when I can let the compiler find them for me? I am guaranteed to make stupid typos and want the computer to help me fix them.
Agreed. Function coloring is a solution (not a problem), one that's better than the alternatives.
The "function coloring problem" people are harming entire ecosystems. In JS for example there are very popular frameworks thay choose to wrap async in sync execution by throwing when encountering async values and re-running parts of the program when the values resolve. The crazy part with these solutions trying to remove coloring, is they don't, they hide it (poorly). So instead of knowing what parts of a program are async you have no idea.
"Just write everything async" is not remotely a good solution to the problem. Not everything needs to be async (in fact most things don't), and it's much harder to reason about async code. The issue is very much not overblown.
Why is async code harder to reason about? I've been using it in C# and the entire point is that it lets you write callbacks in a way that appears nearly identical to synchronous code. If you dive into concurrency (which is a separate thing but can be utilized with async code, such as joining multiple futures at the same time), that parts hard whether you're doing it with async or with explicit threads.
One reason why async-await is trivial in .NET is garbage collector. C# rewrites async functions into a state machine, typically heap allocated. Garbage collector automagically manages lifetimes of method arguments and local variables. When awaiting async functions from other async functions, the runtime does that for multiple async frames at once but it’s fine with that, just a normal object graph. Another reason, the runtime support for all that stuff is integrated into the language, standard library, and most other parts of the ecosystem.
Rust is very different. Concurrency runtime is not part of the language, the standard library defined bare minimum, essentially just the APIs. The concurrency runtime is implemented by “Tokio” external library. Rust doesn’t have a GC; instead, it has a borrow checker who insists on exactly one owner of every object at all times, makes all memory allocations explicit, and exposed all these details to programmer in the type system.
These factors make async Rust even harder to use than normal Rust.
> The concurrency runtime is implemented by “Tokio” external library.
Scare quotes around Tokio?
You can't use Rails without Rails or Django without Django.
The reason Rust keeps this externally is because they didn't want to bake premature decisions into the language. Like PHP's eternally backwards string library functions or Python's bloated "batteries included" standard library chock full of four different XML libraries and other cruft.
> instead, it has a borrow checker who insists on exactly one owner of every object at all times, makes all memory allocations explicit, and exposed all these details to programmer in the type system
Table stakes. Everyone knows this. It isn't hard or scary, it just takes a little bit of getting used to. Like a student learning programming for the first time. It's not even that hard. Anyone can learn it.
It's funny people complain about something so easy. After you learn to ride the bike, you don't complain about learning to ride the bike anymore.
> Rust is very different.
Oh no!
Seriously this is 2025. I can write async Rust without breaking a sweat. This is all being written by people who don't touch the language.
Rust is not hard. Stop this ridiculous meme. It's quite an easy language once you sit down and learn it.
I don't know about C#, but at least in Rust, one reason is that normal (non-async) functions have the property that they will run until they return, they panic, or the program terminates. I.e. once you enter a function it will run to completion unless it runs "forever" or something unusual happens. This is not the case with async functions -- the code calling the async function can just drop the future it corresponds to, causing it to disappear into the ether and never be polled again.
That’s not a solution to the coloring problem any more than making everything red was in 2015 (ie, all the tradeoffs mentioned in the article [0] still apply).
It’s completely overblown. Almost every language with async has the same “problem”.
I’m not calling this the pinnacle of async design, but it’s extremely familiar and is pretty good now. I also prefer to write as much async as possible.
I can't just treat `bar` the same as `foo` because it doesn't give me a String, it might have failed to give me a String. So I need to give it special handling to get a String.
async fn qux() -> String
This also doesn't give me a String. It gives me a thing that can give me a String (an `impl Future<Output=String>`, to be more specific), and I need to give it special handling to get a String.
All of these function have different colours, and I don't really see why it's suddenly a big issue for `qux` when it wasn't for `bar`.
Async closures landed in stable recently and have been a nice QoL improvement, although I had gotten used to working around their absence well enough previously that they haven’t been revolutionary yet from the like “enabling new architectural patterns” perspective or anything like that.
I very rarely have to care about future pinning, mostly just to call the pin macro when working with streams sometimes.
While I'd like to have it, it doesn't stop me from writing a great deal of production code without those niceties.
When it came time for me to undo all the async-trait library hack stuff I wrote after the feature landed in stable, I realized I wasn't really held back by not having it.
Is there a concise document that explains major decisions behind Rust language design for those who know C++? Not a newbie tutorial, just straight to the point: why in-place mutability instead of other options, why encourage stack allocation, what problems with C++ does it solve and at what cost, etc.
I’m not aware of one, but I’m happy to answer questions.
> in-place mutability
I’m not sure what this means as.
> why encourage stack allocation
This is the same as C++, things are stack allocated by default and only put on the heap if you request it. Control is imporrant
> what problems with C++ does it solve and at what cost
The big one here is memory safety by default. You cannot have dangling pointers, iterator invalidation, and the like. The cost is that this is done via compile time checks, and you have to learn how to structure code in a way that demonstrates to the compiler that these properties are correct. That takes some time, and is the difficulty people talk about.
Rust also flips a lot of defaults that makes the language simpler. For example, in C++ terms, everything is trivially relocatable, which means Rust can move by default, and decided to eliminate move constructors. Technically Rust has no constructors at all, meaning there’s no rule of 3 or 5. The feeling of Rust code ends up being different than C++ code, as it’s sort of like “what if Modern C++ but with even more functional influence and barely any OOP.”
Rust has better defaults for types than C++, largely because the C++ defaults came from C. Rust is more ergonomic in this regard. If you designed C++ today, it would likely adopt many of these defaults.
However, for high-performance systems software specifically, objects often have intrinsically ambiguous ownership and lifetimes that are only resolvable at runtime. Rust has a pretty rigid view of such things. In these cases C++ is much more ergonomic because objects with these properties are essentially outside the Rust model.
In my own mental model, Rust is what Java maybe should have been. It makes too many compromises for low-level systems code such that it has poor ergonomics for that use case.
Interestingly, CPU-bound high-performance systems are also incompatible with Rust’s model. Ownership for them is unambiguous, but Rust has another issue, doesn’t support multiple writeable references of the same memory accessed by multiple CPU cores in parallel.
A trivial example is multiplication of large square matrices. An implementation needs to leverage all available CPU cores, and a traditional way to do that you’ll find in many BLAS libraries – compute different tiles of the output matrix on different CPU cores. A tile is not a continuous slice of memory, it’s a rectangular segment of a dense 2D array. Storing different tiles of the same matrix in parallel is trivial in C++, very hard in Rust.
That's the tyranny of Gödel incompleteness (or maybe Rice's theorem, or even both): useful formal systems can be either sound or complete. Rust makes the choice of being sound, with the price of course being that some valid operations not being expressible in the language. C of course works the other way around; all valid programs can be expressed, but there's no (general) way to distinguish invalid programs from valid programs.
For your concrete example of subdividing matrixes, that seems like it should be fairly straightforward in Rust too, if you convert your mutable reference to the data into a pointer, wrap your pointer arithmetic shenanigans in an unsafe block and add a comment at the top saying more or less "this is safe because the different subprograms are always operating on disjoint subsets of the data, and therefore no mutable aliasing can occur"?
I don’t use C++ for most of my applications. I only use C++ to build DLLs which implement CPU-bound performance sensitive numeric stuff, and sometimes to consume C++ APIs and third-party libraries.
Most of my applications are written in C#.
C# provides memory safety guarantees very comparable to Rust, other safety guarantees are better (an example is compiler option to convert integer overflows into runtime exceptions), is a higher level language, great and feature-rich standard library, even large projects compile in a few seconds, usable async IO, good quality GUI frameworks… Replacing C# with Rust would not be a benefit.
It does sound like quite a similar model; unsafe Rust in self contained regions, safe in the majority of areas.
FWIW in the case where you're not separating code via a dynamic library boundary, you give the compiler an opportunity to optimise across those unsafe usages, e.g. inlining opportunities for the unsafe code into callers.
Java should have been like Modula-3, Eiffel, Active Oberon, unfortunately it did not and has been catching up to rethink its design while preserving its ABI.
Thankfully C# has mostly catched up with those languages, as the other language I enjoy using.
After that, is the usual human factor on programming languages adoption.
> However, for high-performance systems software specifically, objects often have intrinsically ambiguous ownership
What is the evidence for this? Plenty of high-performance systems software (browsers, kernels, web servers, you name it) has been written in Rust.
Also Rust does support runtime borrow-checking with Rc<RefCell<_>>. It's just less ergonomic than references, but it works just fine.
Anyone that works on e.g. database kernels that do direct DMA (i.e. all the high-performance ones) experiences this. The silicon doesn’t care about your programming language’s ownership model and will violate it at will. You can’t fix it in the language, you have to accept the behavior of the silicon. Lifetimes are intrinsically ambiguous because objects have neither a consistent nor persistent memory address, a pretty standard property in databases, and a mandatory property of large databases. Yes, you can kind of work around it in idiomatic Rust but performance will not be anything like comparable if you do. You have to embrace the nature of the thing.
The near impossibility of building a competitive high-performance I/O scheduler in safe Rust is almost a trope at this point in serious performance-engineering circles.
To be clear, C++ is not exactly comfortable with this either but it acknowledges that these cases exist and provides tools to manage it. Rust, not so much.
New DB's like Tigerbeetle are written in Zig. Memory control was one of the prime reasons. Rust's custom allocators for the standard library have been a WIP for a decade now.
I think the major decisions behind Rust is being explicit and making the programmer make decisions. No NULLs, no Implicit conversions, no dangling pointers. Lifetimes, Optional, Results, each Match branch needs to exist, etc.
Side note: Stack allocation is faster to execute as there's a higher probability of it being cached.
I would say that RAII is very explicit: Resource Acquisition Is Initialization. When you initialize the struct representing the resource you are acquiring the resource. If you have a struct representing a resource you have the resource. Knowing this, you are also acquiring a call to drop when it goes out of scope. I would argue that the difference here isn't explicitness.
Instead, I would argue that rust is favoring a form of explicitness together with correctness. You have to clean up that resource. I have seen arguments that you should be allowed to leak resources, and I am sympathetic, but if we agree on explicitness as a goal then perhaps you might understand the perspective that a leak should be explicit and not implicit in a the lack of a call a some method. Since linear types are difficult to implement auto-drops are easier if you favor easily doing the correct thing. If you want to leak your resource, stash it in some leak list or unsafe erase it. That is the thing that should be explicit: the unusual choice, not all choices and not the usual choice alone.
But yeah, the drop being implicit in the explicit initialization does lead to developers ignoring it just like a leak being implicit if you forget to call a function often leads to unintentionally buggy programs. So when a function call ends they won't realize that a large number of objects are about to get dropped.
To answer your original question, the rationale is not in one concise location but is spread throughout the various RFCs that lead to the language features.
>> being explicit and making the programmer make decisions
>Why RAII then?
Their quote is probably better rephrased as _being explicit and making the programmer make decisions when the compiler's decision might impact safety_
Implicit conversion between primitives may impact the safety of your application. Implicit memory management and initialization is something the compiler can do safely and is central to Rust's safety story.
Rust is RAII at the compiler level. It's the language spec itself. That's probably the best way to describe the design and intent of Rust's memory model.
When you create a thing, you allocate it. That thing owns it and destroys it, unless you pass that ownership onto something else (which C++ RAII doesn't do very cleanly like Rust can).
Then it does some other nice things to reduce every sharp edge it can:
- No nulls, no exceptions. Really good Option<T> and Result<T,E> that make everything explicit and ensure it gets handled. Wonderful syntactic sugar to make it easy. If you ever wondered if your function should return an error code, set an error reference, throw an exception - that's never a design consideration anymore. Rust has the very best solution in the business. And it does it with rock solid safety.
- Checks how you pass memory between threads with a couple of traits (Send, Sync). If your types don't implement those (usually with atomics and locks), then your code won't pass the complier checks. So multithreaded code becomes provably safe at compile time to a large degree. It won't stop you from deadlocking if you do something silly, but it'll solve 99% of the problems.
- Traits are nicer than classes. You can still accomplish everything you can with classic classes, but you can also do more composition-based inheritance that classes don't give you by bolting traits onto anything you want.
- Rust's standard library (which you don't have to use if you're doing embedded work) has some of the nicest data structures, algorithms, OS primitives, I/O, filesystem, etc. of any language. It's had 40 years of mistakes to learn from and has some really great stuff in it. It's all wonderfully cross-platform too. I frequently write code for Windows, Mac, and Linux and it all just works out of the box. Porting is never an issue.
- Rust's functional programming idioms are super concise and easy to read. The syntax isn't terse.
- Cargo is the best package manager on the planet right now. You can easily import a whole host of library functionality, and the management of those libraries and their features is a breeze. It takes all of sixty seconds to find something you want and bring it into your codebase.
- You almost never need to think about system libraries and linking. No Makefiles, no Cmake, none of that build complexity or garbage. The compiler and cargo do all of the lifting for you. It's as easy as python. You never have to think about it.
In-place mutability and stack allocation are for speed. This made them have variables and values inside them both as separate first-class citizens. The entire borrow checker is just for tracking variables access so that you don't need to clone values. I'd say that given this one guiding decision there are very little arbitrary ones in Rust. The rest of it pretty much had to be made exactly the way it is. Rust is more of a discovery than a construct.
C++ is just Rust without any attempt at tracking variable access and cloning which leads to a mess because people are too terrible at that to do that manually and ad-hoc. So Rust fixes that.
This reads like a list of symptoms of what's wrong with the ergonomics of Rust. This is not to bash Rust. It has its uses. But you need to balance what you are sacrificing for what you are getting.
Bonus: do it with no heap allocation. This actually makes it easier because you basically don’t deal with lifetimes. You just have a state object that you pass to your input system, then your guest cpu system, then your renderer, and repeat.
My second (and final) rust project was a gameboy emulator that basically worked the same way.
But one of the best things about learning by writing an emulator is that there’s enough repetition you begin looking for abstractions and learn about macros and such, all out of self discovery and necessity.
I’ve found emulators to be a pretty poor first project for rust specifically for the reasons you alluded to: That you need to know to write it without heap allocation (or other hoop jumping so long as you avoid juggling lifetimes) when so much literature and example emulator code doesn’t do this is a recipe for a bad experience. Ask me how I know.
If you’re going to write an emulator in this style, why even use an imperative language when something like Haskell is designed for this sort of thing?
These emulators already exist in basically every language, so why do anything? The point is the journey, which doesn’t need to be the shortest, most optimal path possible.
I’m saying it’s not optimal for learning the language, not that it’s not worth doing. I’ve worked on 3 different emulators for fun over the last few years, my first in rust. It was a bad experience for learning rust because I was following prior art which relied heavily on shared data structures and lots of poking randomly at blocks of RAM, a very natural way to think when you’re engrossed in the mechanics of an 8-bit CPU.
I had a better time writing a raycaster and later a path tracer, although by then I had learned to avoid dealing with the borrow checker…
Sorry, but it's easier to learn basic C++/C, and let beginner developers get lessons from a basic linter or clang-tidy for about the same result, for a fraction of the developer cost.
I want rust to be adopted and I believe companies should force it, but you will not get adoption from young developers and even less from senior C++ developers.
Not to mention rewriting existing C++ code in rust, which cost would be astronomical, although I do believe companies should invest in rewriting things in rust because it's the right thing to do.
You’re passing in two references and returning a reference.
The compiler knows the returned reference must be tied to one of the incoming references (since you cannot return a reference to something created within the function, and all inputs are references, the output must therefore be referencing the input). But the compiler can’t know which reference the result comes from unless you tell it.
Theoretically it could tell by introspecting the function body, but the compiler only works on signatures, so the annotation must be added to the function signature to let it determine the expected lifetime of the returned reference.
> Theoretically it could tell by introspecting the function body, but the compiler only works on signatures
Note that this is an intentional choice rather than a limitation, because if the compiler analyzed the function body to determine lifetimes of parameters and return values, then changing the body of a function could be a non-obvious breaking API change. If lifetimes are only dependent on the signature, then its explicit what promises you are or are not making to callers of a function about object lifetimes, and changing those promises must be done intentionally by changing the signature rather than implicitly.
Oh yes, I didn’t mean to make it sound like a problem. I personally strongly prefer signature-based typing vs like in Typescript where you can easily be returning an entirely unintentional type and not realize it until you try to use it in an explicitly typed context down the line.
I also imagine it’s much faster for the type-checking pass of the compiler to just look at the signatures.
Compiler can figure that out, but the thing is compiler needs also to understand lifetimes at the site where this function is called. In general case compiler will not look into the code of a called function to see what it does, compiler relies on a function declaration.
That `longest` if defined without explicit lifetimes treated like a lifetime of a return value is the same as of the first argument. It is a rule "lifetime elision", which allows to not write lifetimes explicitly in most cases.
But `longest` can return a second reference also. With added lifetimes the header of the function says exactly that: the lifetime of a return value is a minimum of lifetimes of arguments. Not the lifetime of the first one.
To make a compiler automatically handle all of the cases like that, you will need to do an extensive static analysis, which would make compiling take forever.
> Use String and clone() and unwrap generously; you can always refactor later
At that point you might as well be writing Java or Go or whatever though. GC runtimes tend actually to be significantly faster for this kind of code, since they can avoid all those copies by sharing the underlying resource. By the same logic, you can always refactor the performance-critical stuff via your FFI of choice.
So long as you're aware that you're not optimizing, it's fine. Trying to build something useful as a new Rust dev while worrying about lifetimes is going to be quite challenging, unless your intention is to specifically learn about lifetimes and the borrow checker.
Yes the borrow checker is central to Rust, but there are other features to the language that people _also_ need to learn and explore to be productive. Some of these features may attract them to Rust (like pattern matching / traits / etc.)
> At that point you might as well be writing Java or Go or whatever though.
And miss Option, Result, proper enums, powerful pattern matching, exhaustive pattern matching, affine types, traits, doctests... and the many other QoL features that I sorely miss when I drop to e.g. TS/Node.
I'm not using Rust for the borrow checker, but it's nice to have when I need it to hold my hand and not that much of an issue when I don't. I wanted to like Go but I just can't.
Dropping to no_std though... that was a traumatic experience.
"Significantly" and "this kind" are load bearing sentences here. In applications where predictable latency is desired, cloning is better than GC.
This is also the baby steps of learning the language. As a programmer gets better they will recognize when they are making superflous clones. Refactoring performance-critical stuff in FFI, however, is painful and wont get easier with time.
Furthermore, in real applications, this only really applies to Strings and vectors. In most of my applications most `clones` are of reference types - which is only marginally more expensive than memory sharing under a GC.
I went through this the first year that I did Advent of Code in rust, like okay I read in all the strings from the input file and now they're in a vector, so I'm going to iterate the vector and add references to those strings into this other structure, but of course they're still owned by the original vector, that's awkward. Oh wait I can iter_into and then I get owned objects and that ownership can be transferred to the other structure instead, but now I need them to also be keys in a map, do I use references for that too?
Cloning small objects is lightning fast, turns out in a lot of these cases it makes sense to just do the clone, especially when it's a first pass. The nice thing is that at least rust makes you explicitly clone() so you're aware when it's happening, vs other languages where it's easy to lose track of what is and isn't costing you memory. So you can see that it's happening, you can reason about it, and once the bones of the algorithm are in place, you can say "okay, yes, this is what should ultimately own this data, and here's the path it's going to take to get there, and these other usages will be references or clones.
It's really not, it's the way python works. Heap allocations are "fast" on modern CPUs that are too fast to measure for most stuff, but they're much (much) slower than the function call and code you're going to use to operate on whatever the thing it was you cloned.
Code that needs memory safety and can handle performance requirements like this has many options for source language, almost none of which require blog posts to "flatten the learning curve".
(And to repeat: it's much slower than a GC which doesn't have to make the clone at all. Writing Rust that is "Slower Than Java" is IMHO completely missing the point. Java is boring as dirt, but super easy!)
If the object had a stack-bounded lifetime, the borrow checker would have been able to prove the analysis though. The advice is to clone things it can't, which pretty much requires that it go into the general heap. I'm sure there are some interesting counterexamples, but the situation you're imagining seems kinda academic.
I think this is just a mistake, in that “a bit like” is correct. The ways in which it are different depend on which language you are taking the concept of “interface” from, but the statement that it’s like one is accurate.
For me the most important thing about Rust is to understand that it's a langue with value semantics. Which makes it completely different than every mainstream language you encountered so far.
Variable in rust is not a label you can pass around and reuse freely. It's a fixed size physical memory that values can be moved into or moved out of. Once you understand that everything makes sense. The move semantics, cloning, borrowing, Sized, impl. Every language design element of rust is a direct consequence of that. It's the values that get created, destroyed and moved around and variables are actual first-class places to keep them with their own identity separate from values that occupy them. It's hard to notice this because Rust does a lot to pretend it's a "normal" language to draw people in. But for anyone with experience in programming that attempts to learn Rust I think this realization could make the process at least few times easier.
It's hard to shift to this new paradigm and embrace it, so in the meantime feel use a lot of Rc<> and cloning if you just need to bang out some programs like you would in any other mainstream language.
For people who don't get the reference, this might be referring to the notoriously gnarly task of implementing a doubly-linked lists in Rust [1]
It is doable, just not as easy as in other languages because a production-grade linked-list is unsafe because Rust's ownership model fundamentally conflicts with the doubly-linked structure. Each node in a doubly-linked list needs to point to both its next and previous nodes, but Rust's ownership rules don't easily allow for multiple owners of the same data or circular references.
You can implement one in safe Rust using Rc<RefCell<Node>> (reference counting with interior mutability), but that adds runtime overhead and isn't as performant. Or you can use raw pointers with unsafe code, which is what most production implementations do, including the standard library's LinkedList.
Rust still needs a way out of that mess. It's conceptually possible to have compile time checking for this. Think of RefCell/Weak and .upgrade() and .borrow() being checked at compile time.
I've discussed this with some of the Rust devs. The trouble is traits. You'd need to know if a trait function could borrow one of its parameters, or something referenced by one of its parameters. This requires analysis that can't be done until after generics have been expanded. Or a lot more attributes on trait parameters. This is a lot of heavy machinery to solve a minor problem.
In practice, it really doesn't. The difficulty of implementing doubly linked lists has not stopped people from productively writing millions of lines of Rust in the real world. Most programmers spend less than 0.1% of their time reimplementing linked data structures; rust is pretty useful for the other 99.9%.
Apologies since I have not taken the time to learn rust yet, but I've written a lot of modern C++. Is the ownership model kind of like std::unique_ptr and std::move, and `Rc<RefCell<Node>>` the same idea as `std::shared_ptr`? But less idiomatic? Or do I have the wrong idea?
Not really, because Rust enforces a "many readers or one writer" invariant on everything that has no C++ equivalent. That invariant is precisely what makes the doubly-linked list case hard (because every interior node in the list would be readable from two places, which means it can never be written to).
If that's a specific use case you need to handle, it's O(1) again if you have a pointer to both the node to be removed and the previous node.
Whether it's more efficient to carry a second pointer around when manipulating the list, or store a second pointer in every list node (aka double linked list) is up to your problem space.
So that you learn that loaning is for giving temporary shared^exclusive access within a statically-known scope, and not for storing data.
Trying to construct permanent data structures using non-owning references is a very common novice mistake in Rust. It's similar to how users coming from GC languages may expect pointers to local variables to stay valid forever, even after leaving the scope/function.
Just like in C you need to know when malloc is necessary, in Rust you need to know when self-contained/owning types are necessary.
The biggest thing I’ve run into where I really want self-referential types is for work that I want to perform once and then cache, while still needing access to the original data.
An example: parsing a cookie header to get cookie names and values.
In that case, I settled on storing indexes indicating the ranges of each key and value instead of string slices, but it’s obviously a bit more error prone and hard to read. Benchmarking showed this to be almost twice as fast as cloning the values out into owned strings, so it was worth it, given it is in a hot path.
I do wish it were easier though. I know there are ways around this with Pin, but it’s very confusing IMO, and still you have to work with pointers rather than just having a &str.
It's not that it doesn't understand doubly linked list. It's just that you don't understand their consequences and have no objections against turning your program state briefly into inconsistent bullshit to facilitate them. The compiler minds. Unless you use Rc<>. That's what this language has for expressing inconsistency. Or unsafe {} if you are cocky. Borrows are not named pointers for a reason.
For me it was like Haskell. I spent an afternoon on it, my brain hurt too much, and I relegated it to the category of languages that are too complicated for what I need to do with a computer.
Languages I liked, I liked immediately. I didn’t need to climb a mountain first.
The thing is, once you internalized the concepts (ownership, borrowing, lifetimes), it's very hard to remember what made it difficult in the first place. It's "curse of knowledge" in some sense.
What's changed since 2015 is that we ironed out some of the wrinkles in the language (non-lexical lifetimes, async) but the fundamental mental model shift required to think in terms of ownership is still a hurdle that trips up newcomers.
100%. Newcomers still struggle a bit, especially if they've never used C/C++ before.
A good way to get people comfortable with the semantics of the language before the borrow checker is to encourage them to clone() strings and structs for a bit, even if the resulting code is not performant.
Once they dip their toes into threading and async, Arc<Lock<T>> is their friend, and interior mutability gives them some fun distractions while they absorb the more difficult concepts.
Do you mean `Arc<Mutex<T>>`? Yeah, I agree.
Wrote a blog post on that topic as well: https://corrode.dev/blog/prototyping/
The title is a bit of a misnomer, but it's about beginner-friendly escape hatches in the language. Perhaps it's useful to newcomers.
Unfortunately, yes. I still end up writing C++ instead of Rust for low-level system stuff. Since I also know Go - I usually prefer that when I need lean, middleware services. Learned Rust (somewhat) with great difficulty but still don't use it anywhere. Still haven't figured out how to design effectively using Rust and approaches suggested in that article like clone()/unwrap() stuff and refactor later just leave a bad taste in that mouth.
It's like reading "A Discipline of Programming", by Dijkstra. That morality play approach was needed back then, because nobody knew how to think about this stuff.
Most explanations of ownership in Rust are far too wordy. See [1]. The core concepts are mostly there, but hidden under all the examples.
That explains the model. Once that's understood, all the details can be tied back to those rules.[1] https://doc.rust-lang.org/book/ch04-01-what-is-ownership.htm...
Maybe it's my learning limitations, but I find it hard to follow explanations like these. I had similar feelings about encapsulation explanations: it would say I can hide information without going into much detail. Why, from whom? How is it hiding if I can _see it on my screen_.
Similarly here, I can't understand for example _who_ is the owner. Is it a stack frame? Why would a stack frame want to move ownership to its callee, when by the nature of LIFO the callee stack will always be destroyed first, so there is no danger in hanging to it until callee returns. Is it for optimization, so that we can get rid of the object sooner? Could owner be something else than a stack frame? Why can mutable reference be only handed out once? If I'm only using a single thread, one function is guaranteed to finish before the other starts, so what is the harm in handing mutable references to both? Just slap my hands when I'm actually using multiple threads.
Of course, there are reasons for all of these things and they probably are not even that hard to understand. Somehow, every time I want to get into Rust I start chasing these things and give up a bit later.
> Why would a stack frame want to move ownership to its callee
Rust's system of ownership and borrowing effectively lets you hand out "permissions" for data access. The owner gets the maximum permissions, including the ability to hand out references, which grant lesser permissions.
In some cases these permissions are useful for performance, yes. The owner has the permission to eagerly destroy something to instantly free up memory. It also has the permission to "move out" data, which allows you to avoid making unnecessary copies.
But it's useful for other reasons too. For example, threads don't follow a stack discipline; a callee is not guaranteed to terminate before the caller returns, so passing ownership of data sent to another thread is important for correctness.
And naturally, the ability to pass ownership to higher stack frames (from callee to caller) is also necessary for correctness.
In practice, people write functions that need the least permissions necessary. It's overwhelmingly common for callees to take references rather than taking ownership, because what they're doing just doesn't require ownership.
I think your comment has received excellent replies. However, no one has tackled your actual question so far:
> _who_ is the owner. Is it a stack frame?
I don’t think that it’s helpful to call a stack frame the owner in the sense of the borrow checker. If the owner was the stack frame, then why would it have to borrow objects to itself? The fact that the following code doesn’t compile seems to support that:
User lucozade’s comment has pointed out that the memory where the object lives is actually the thing that is being owned. So that can’t be the owner either.So if neither a) the stack frame nor b) the memory where the object lives can be called the owner in the Rust sense, then what is?
Could the owner be the variable to which the owned chunk of memory is bound at a given point in time? In my mental model, yes. That would be consistent with all borrow checker semantics as I have understood them so far.
Feel free to correct me if I’m not making sense.
I believe this answer is correct. Ownership exists at the language level, not the machine level. Thinking of a part of the stack or a piece of memory as owning something isn’t correct. A language entity, like a variable, is what owns another object in rust. When that object goes at a scope, its resources are released, including all the things it owns.
> Why can mutable reference be only handed out once?
Here's a single-threaded program which would exhibit dangling pointers if Rust allowed handing out multiple references (mutable or otherwise) to data that's being mutated:
> Why would a stack frame want to move ownership to its callee, when by the nature of LIFO the callee stack will always be destroyed first, so there is no danger in hanging to it until callee returns.
It definitely takes some getting used to, but there's absolutely times when you could want something to move ownership into a called function, and extending it would be wrong.
An example would be if it represents something you can only do once, e.g. deleting a file. Once you've done it, you don't want to be able to do it again.
> _who_ is the owner. Is it a stack frame?
The owned memory may be on a stack frame or it may be heap memory. It could even be in the memory mapped binary.
> Why would a stack frame want to move ownership to its callee
Because it wants to hand full responsibility to some other part of the program. Let's say you have allocated some memory on the heap and handed a reference to a callee then the callee returned to you. Did they free the memory? Did they hand the reference to another thread? Did they hand the reference to a library where you have no access to the code? Because the answer to those questions will determine if you are safe to continue using the reference you have. Including, but not limited to, whether you are safe to free the memory.
If you hand ownership to the callee, you simply don't care about any of that because you can't use your reference to the object after the callee returns. And the compiler enforces that. Now the callee could, in theory give you back ownership of the same memory but, if it does, you know that it didn't destroy etc that data otherwise it couldn't give it you back. And, again, the compiler is enforcing all that.
> Why can mutable reference be only handed out once?
Let's say you have 2 references to arrays of some type T and you want to copy from one array to the other. Will it do what you expect? It probably will if they are distinct but what if they overlap? memcpy has this issue and "solves" it by making overlapped copies undefined. With a single mutable reference system, it's not possible to get that scenario because, if there were 2 overlapping references, you couldn't write to either of them. And if you could write to one, then the other has to be a reference (mutable or not) to some other object.
There are also optimisation opportunities if you know 2 objects are distinct. That's why C added the restrict keyword.
> If I'm only using a single thread
If you're just knocking up small scripts or whatever then a lot of this is overkill. But if you're writing libraries, large applications, multi-dev systems etc then you may be single threaded but who's confirming that for every piece of the system at all times? People are generally really rubbish at that sort of long range thinking. That's where these more automated approaches shine.
> hide information...Why, from whom?
The main reason is that you want to expose a specific contract to the rest of the system. It may be, for example, that you have to maintain invariants eg double entry book-keeping or that the sides of a square are the same length. Alternatively, you may want to specify a high level algorithm eg matrix inversion, but want it to work for lots of varieties of matrix implementation eg sparse, square. In these cases, you want your consumer to be able to use your objects, with a standard interface, without them knowing, or caring, about the detail. In other words you're hiding the implementation detail behind the interface.
That's not explaining ownership, that motivating it. Which is fine. The thing that's hard to explain and learn is how to read function signatures involving <'a, 'b>(...) -> &'a [&'b str] or whatever. And how to understand and fix the compiler errors in code calling such a function.
Is it a lot different from std::unique_ptr in C++?
I thought the Rust Book was too verbose but I liked Comprehensive Rust: https://google.github.io/comprehensive-rust/
I felt like I understood the stuff in the book based on cursory reading, but I haven't tried to actually use it.
>Is it a lot different from std::unique_ptr in C++?
Is knowing C++ a pre-requisite?
IME teaching students Rust, knowing C++ first actually is a detriment to learning because they have a bunch of C++ habits to unlearn. Those students "fight the borrow checker" much more than the blank slate students, because they have some idea about how code "should" be written.
I would say knowing the useless features of c++ is a pre-requisite for learning rust yes. It's yet another c++ replacement designed by a committee of phds.
You would think they would be smart enough to realize that a language taking X hours to learn is a language flaw not a user flaw, but modern education focuses on specialization talents rather than general intelligence.
The goal of some languages might be to be easy to learn. But most "system" languages focus on helping design good software, where "good" might mean reliable, maintainable or performant.
Writing good software most often is not easy. The learning curve of a particular language usually is only a modest part of what it takes.
Right but Rust is supposed to be a systems language. We already have dozens of languages that are easy to learn. The whole reason for having compile time memory management is to satisfy the constraints of a systems language...
I don't think it's much harder than learning C or C++ which are the only comparable mainstream languages.
I think it's actually easier, thanks to Cargo and the Crates ecosystem. Some of the hardest things for students are just building and linking code, especially third party libraries.
I run two intermediate programming courses, one where we teach C++, and another where we teach Rust. In the Rust course, by the first week they are writing code and using 3rd party libraries; whereas in the C course we spend a lot of time dealing with linker errors, include errors, segfaults, etc. The learning curve for C/C++ gets steep very fast. But with Rust it's actually quite flat until you have to get into borrowing, and you can even defer that understanding with clone().
By the end of the semester in the C++ course, students' final project is a file server, they can get there in 14 weeks.
In Rust the final project is a server that implements LSP, which also includes an interpreter for a language they design. The submissions for this project are usually much more robust than the submissions for the C++ course, and I would attribute this difference to the designs of the languages.
> Is it a lot different from std::unique_ptr in C++?
It’s both identical and very different, depending on the level of detail you want to get into. Conceptually, it’s identical. Strictly speaking, the implementations differ in a few key ways.
The good news is that idiomatically written good clean Rust code doesn't need to rely on such borrow signatures very often. That's more when you're leaving the norm and doing something "clever."
I know it throws people off, and the compiler error can be confusing, but actual explicit lifetimes as part of a signature are less common than you'd expect.
To me it's a code smell to see a lot of them.
Summarizing a set of concepts in a way that feels correct and complete to someone who understands them, is a much easier task than explaining them to someone who doesn't. If we put this in front of someone who's only worked with call-by-sharing languages, do you think they'll get it right away? I'm skeptical.
For me it really clicked when I realized ownership / lifetimes / references are just words used to talk about when things get dropped. Maybe because I have a background in C so I'm used to manual memory management. Rust basically just calls 'free' for you the moment something goes out of scope.
All the jargon definitely distracted me from grasping that simple core concept.
Almost all of it.
Rust also has the “single mutable reference” rule. If you have a mutable reference to a variable, you can be sure nobody else has one at the same time. (And the value itself won’t be mutated).
Mechanically, every variable can be in one of 3 modes:
1. Directly editable (x = 5)
2. Have a single mutable reference (let y = &mut x)
3. Have an arbitrary number of immutable references (let y = &x; let z = &x).
The compiler can always tell which mode any particular variable is in, so it can prove you aren’t violating this constraint.
If you think in terms of C, the “single mutable reference” rule is rust’s way to make sure it can slap noalias on every variable in your program.
This is something that would be great to see in rust IDEs. Wherever my cursor is, it’d be nice to color code all variables in scope based on what mode they’re in at that point in time.
"Rust basically just calls 'free' for you the moment something goes out of scope."
C++ does that too with RAII. Go ahead and use whatever STL containers you like, emplace objects onto them, and everything will be safely single-owned with you never having to manually new or delete any of it.
The difference is that C++'s guarantees in this regard derive from a) a bunch of implementation magic that exists to hide the fact that those supposedly stack-allocated containers are in fact allocating heap objects behind your back, and b) you cooperating with the restrictions given in the API docs, agreeing not to hold pointers to the member objects or do weird things with casting. You can use scoped_ptr/unique_ptr but the whole time you'll be painfully aware of how it's been bolted onto the language later and whenever you want you can call get() on it for the "raw" underlying pointer and use it to shoot yourself in the foot.
Rust formalizes this protection and puts it into the compiler so that you're prevented from doing it "wrong".
> a bunch of implementation magic that exists to hide the fact that those supposedly stack-allocated containers are in fact allocating heap objects behind your back
The heap is but one source for allocator-backed memory. I've used pieces of stack for this, too. One could also use an entirely staticly sized and allocated array.
the tradeoff is that ~you have to guess where rust is doing the frees, and you might be wrong. in the end this would be strictly equivalent to an explicit instruction to free, with the compiler refusing to compile if the free location broke the rules.
It's really too bad rust went the RAII route.
There's no guessing - Rust has well defined drop order. It also has manual drop, should you wish to override the defined order.
sorry i shouldn't have said guess. i meant consider
How often do you care about the order in which objects are dropped?
anything where you need to have stuff run in constant time.
Okay, but an IDE could just visualize that for you. A linter rule could force you to manually drop if you want to be explicit.
[dead]
Right. If you come to Rust from C++ and can write good C++ code, you see this as "oh, that's how to think about ownership". Because you have to have a mental model of ownership to get C/C++ code to work.
But if you come from Javascript or Python or Go, where all this is automated, it's very strange.
The list in the above comment isn’t a summary — it’s a precise definition. It can and must be carefully explained with lots of examples, contrasts with other languages, etc., but the precise definition itself must figure prominently, and examples and intuition should relate back to it transparently.
Practically, I think it suggests that learning the borrow checker should start with learning how memory works, rather than any concepts specific to Rust.
And, after someone who doesn't know rust reads this neat and nice summary, they would still know nothing about rust. (Except "this language's compiler must have some black magic in it.")
I think the most important lesson is this:
Ownership is easy, borrowing is easy, what makes the language super hard to learn is that functions must have signatures and uses that together prove that references don't outlive the object.
Also: it's better not store referenced object in a type unless it's really really needed as it makes the proof much much more complex.
> Ownership is easy, borrowing is easy
100%. It's the programmer that needs to adapt to this style. It's not hard by any means at all, it just takes some adjustment.
Indeed. Programmers are holding Rust wrong.
Programmers new to Rust, you mean.
It's kind of like career Java programmers using JavaScript or Python for the first time and bringing their way of doing things.
This explanation doesn't expose anything meaningful to my mind, as it doesn't define ownership and borrowing, both words being apparently rooted in an analogy with financial asset management.
I'm not acquainted with Rust, so I don't really know, but I wonder if the wording plays a role in the difficulty of concept acquisition here. Analogies are often double edged tools.
Maybe sticking to a more straight memory related vocabulary as an alternative presentation perspective might help?
The way I think about it is more or less in terms of how a C program would work: if you assume a heap allocated data structure, the owner is the piece of code that is responsible for freeing the allocation at the appropriate time. And a reference is just a pointer with some extra compile time metadata that lets the borrow checker prove that the reference doesn’t outlive the referent and that there’s no mutable aliasing.
If you've worked inside of CPython or other programs with manual reference counting, the idea of borrowing shows up there, where you receive a reference from another part of the program and then mess with the object without tweaking the reference count, "borrowing" an existing reference because any copies you've of the address will be short lived. The term shows up throughout CPython.
I find it strange that you relate borrowing and ownership to financial asset management.
From that angle, it indeed doesn’t seem to make sense.
I think, but might be completely wrong, that viewing these actions from their usual meaning is more helpful: you own a toy, it’s yours to do as tou please. You borrow a toy, it’s not yours, you can’t do whatever you want with it, so you can’t hold on to it if the owner doesn’t allow it, and you can’t modify it for the same reasons.
Analogies often leak.
1. In real life I can borrow a toy from you and while I have that toy in my hands, the owner can exchange ownership with somebody else, while the object is borrowed by me. I.e. in real life the borrowing is orthogonal to ownership. In rust you can't do that.
2. Borrowing a toy is more akin to how mutable references work in rust. Immutable references allow multiple people to play with the same toy simultaneously, provided they don't change it.
Analogies are just analogies
What do you mean with usual sense? Maybe it's "financial" that put the interpretation out of the track, but financial comes fidus, that is trust, as in trust that outcomes of reality will meet some expectation of a mental representation.¹
"You own a toy" is the first thing a child is teached as wrong assumption by reality if not by careful social education, isn't it? The reality is, "you can play with the toy in some time frame, and sharing with others is the only way we can all benefit of joyful ludic moment, while claims of indefinite exclusive use of the toy despite limited attention span that an individual can spend on it is socially detrimental."
Also memory as an abstract object pragmatically operate on very different ground than a toy. If we could duplicate any human hand grabbable object as information carried by memory holding object, then any economy would virtually be a waste of human attention.
¹ edit: actually I was wrong here, I have been in confusion with "fiduciary". Finance instead comes from french "fin"(end), as in "end of debt".
Many people can borrow your toy to have look at it, but only one person can borrow it and play with it. And they are only allowed to play while no one is watching. And if you want to modify your toy with some tool it's not your's anymore, it yas moved and now belongs to the tool.
I guess I'm trying to say that analogy is of limited use here.
You think borrowing a lawn mower or owning a power drill is financial asset management?
On the most abstract level, even "I" and "think" are misleading notions of what’s passing through current attention. So "borrowing" and "owning" are not really great starting point notions to "think" in that sense. But on the more mundane level of mentally handling stuffs, that’s an analogy that can have its own merits (and flaws, of course).
That really doesn't explain the model because you have completely left out the distinction between exclusive/shared (or mutable/immutable) borrows. Rust made a large number of choices with respect to how it permits such borrows and those do not follow from this brief outline nor from intuition or common sense. For example, the no aliasing rule is motivated not by intuition or common sense but from a desire to optimize functions.
The most complicated aspect of the borrows comes about from the elision rules which will silently do the wrong thing and will work fantastically until they don't at which point the compiler error is pointing at a function complaining about a lifetime parameter of a parameter with the trait method implying that the parameter has to live too long but the real problem was a lifetime in the underlying struct or a previous broken lifetime bound. Those elision rules are again not-intuitive and don't fall out of your explanation axiomatically. They were decisions that were made to attempt to simplify the life of programmers.
I often wanted to find writings about the 60s on how they approached system/application state at assembly level. I know Sutherland Sketchpad thesis has a lot of details about data structures but I never read it (except for 2-3 pages).
I think the Brown University’s modifications to the rust book do an excellent job of explaining the borrow checker.
Seems incomplete. E.g. what happens if a borrower goes away?
It stops being borrowed?! What kind of question is this.
A question about definitions. Some other options would be:
Assuming the best possible outcome in case of missing information turns out to be a bad strategy in general.>the real owner has to be a reference-counted cell.
And what is that? Its easy to fall in the trap of making explanations that is very good (if you already understand).
The only way I could understand the borrow-checker was to implement my own version. Then it made sense.
The second bullet in the second section is overpromising badly. In fact there are many, many, many ways to write verifiably correct code that leaves no dangling pointers yet won't compile with rustc.
Frankly most of the complexity you're complaining about stems from attempts to specify exactly what magic the borrow checker can prove correct and which incantations it can't.
A great teaching technique I learned from a very good match teacher is that when explaining core concepts, the simplified definitions don't need to be completely right. They are much simpler to grasp and adding exceptions to these is also quite easy compared to trying to understand correct, but complex, definitions at the beginning.
Yeah, but the whole purpose here is "flattening the learning curve", and telling people code will work when it won't is doing the opposite.
That bullet, at its most charitable, defines the "idealized goal" of the borrow collector. The actual device is much less capable (as it must be, as the goal is formally undecidable!), and "learning rust" requires understanding how.
> ... defines the "idealized goal" of the borrow collector. The actual device is much less capable
I think here you expanded on the original point in a good way. I would then continue with adding additional set of points covering the issue in greater detail and a set of examples of where this commonly happens and how to solve it.
Ironically, most people understand "learning curves" counterintuitively.
If a "learning curve" is a simple X-Y graph with "time" and "knowledge" being on each axis respectively, then what sort of learning curve is preferable: a flatter one or a steep one?
Clearly, if you graph large increases of knowledge over shorter periods of time, a steeper learning curve is more preferable. "Flattening the learning curve" makes it worse!
But for some reason, people always reverse this meaning, and so the common idiom breaks down for people who try to reason it out.
Replace "knowledge" with "required knowledge". It's not about how efficiently you can learn, but how much do you need to learn in a specific amount of time. If you need to learn a lot in short amount of time (which is a hard thing to do) the curve is steep. You can flatten the curve by increasing the time you have available or by requiring less knowledge.
As a systems programmer I found Rust relatively easy to learn, and wonder if the problem is non-systems programmers trying to learn their first systems language and having it explicitly tell them "no, that's dangerous. no, that doesn't make sense". If you ask a front end developer to suddenly start writing C they are going to create memory leaks, create undefined behavior, create pointers to garbage, run off the end of an array, etc. But they might "feel" like they are doing great because there program compiles and sort of runs.
If you have already gotten to the journeyman or mastery experience level with C or C++ Rust is going to be easy to learn (it was for me). The concepts are simply being made explicit rather than implicit (ownership, lifetimes, traits instead of vtables, etc).
It took me a few tries to get comfortable with Rust—its ownership model, lifetimes, and pervasive use of enums and pattern matching were daunting at first. In my initial attempt, I felt overwhelmed very early on. The second time, I was too dogmatic, reading the book line by line from the very first chapter, and eventually lost patience. By then, however, I had come to understand that Rust would help me learn programming and software design on a deeper level. On my third try, I finally found success; I began rewriting my small programs and scripts using the rudimentary understanding I had gained from my previous encounters. I filled in the gaps as needed—learning idiomatic error handling, using types to express data, and harnessing pattern matching, among other techniques.
After all this ordeal, I can confidently say that learning Rust was one of the best decisions I’ve made in my programming career. Declaring types, structs, and enums beforehand, then writing functions to work with immutable data and pattern matching, has become the approach I apply even when coding in other languages.
I had quite a similar experience. During the 3rd attempt at learning, everything seemed to click and I was able to be effective at writing a few programs.
This is all despite a long career as a programmer. Seems like some things just take repetition.
The "Dagger" dependency injection framework for the JVM took me 3 'learning attempts' to understand as well. May say more about myself than about learning something somewhat complicated.
Your experience matches an observation I have made, that when C++ developers approach Rust for the first time they often "fight the borrow checker" when they use C++ idioms in Rust. Then they start to learn Rust idioms, and bring them back to C++, which causes them to write more robust code despite not having and borrow checking at all.
> Stop resisting. That’s the most important lesson
> Accept that learning Rust requires...
> Leave your hubris at home
> Declare defeat
> Resistance is futile. The longer you refuse to learn, the longer you will suffer
> Forget what you think you knew...
Now it finally clicked to me that Orwell's telescreen OS was written in Rust
But it is true. My own biggest mistake when learning Rust was that I tried to torce Object Oriented paradigms on it. That went.. poorly. As soon as I went "fuck it, I just do it like you want" things went smoothly.
Sounds like an abusive relationship if im being honest. Your programming language shouldnt constrict you in those ways.
>Your programming language shouldn't constrict you in those ways
Says who? Programming languages come in all shapes and sizes, and each has their tradeoffs. Rust's tradeoff is that the compiler is very opinionated about what constitutes a valid program. But in turn it provides comparable performance to C/C++ without many of the same bugs/security vulnerabilities.
Every programming language has constrictions by the nature of having syntax.
In JavaScript you can declare a variable, set it to 5 (number), and then set it to the "hello" (string), but that's not allowed in e.g. C. Is C constricting me too much because I have to do it in C's way?
I believe you can do that in C pretty easily with a void pointer, someone correct me if I'm mistaken.
Should you? Different question entirely.
It helps to understand the cultural ethos of the original rust devs, and the situation that gave rise to it.
Abusive relationships involve coercion, control, fear, and often violate personal autonomy and consent. One party dominates the other in a harmful way. Using Rust is not harmful.
Placing restrictions on the programs a programmer can write is not abusive. The rules exist to ensure clarity, safety, performance, and design goals. In an abusive relationship, rules are created to control or punish behavior, often changing capriciously and without reason or consultation. By contrast, Rust is designed by a group of people who work together to advance the language according to a set of articulated goals. The rules are clear and do not change capriciously.
Abuse causes emotional trauma, isolation, and long-term harm. Rust may cause feelings of frustration and annoyance, it may make you a less efficient programmer, but using it does not cause psychological or physical harm found in abusive relationships.
Rust has a few big hurdles for new users:
- it's very different from other languages. That's intentional but also an obstacle.
- it's a very complex language with a very terse syntax that looks like people are typing with their elbows and are hitting random keys. A single character can completely change the meaning of a thing. And it doesn't help that a lot of this syntax deeply nested.
- a lot of its features are hard to understand without deeper understanding of the theory behind them. This adds to the complexity. The type system and the borrowing mechanism are good examples. Unless you are a type system nerd a lot of that is just gobblygook to the average Python or Javascript user. This also makes it a very inappropriate language for people that don't have a master degree in computer science. Which these days is most programmers.
- it has widely used macros that obfuscate a lot of things that further adds to the complexity. If you don't know the macro definitions, it just becomes harder to understand what is going on. All languages with macros suffer from this to some degree.
I think LLMs can help a lot here these days. When I last tried to wrap my head around Rust that wasn't an option yet. I might have another go at it at some time. But it's not a priority for me currently. But llms have definitely lowered the barrier for me to try new stuff. I definitely see the value of a language like users. But it doesn't really solve a problem I have with the languages I do use (kotlin, python, typescript, etc.). I've used most popular languages at some point in my life. Rust is unique in how difficult it is to learn.
>it's very different from other languages. That's intentional but also an obstacle.
It's very different from a lot of the languages that people are typically using, but all the big features and syntax came from somewhere else. See:
>The type system and the borrowing mechanism are good examples. Unless you are a type system nerd a lot of that is just gobblygook to the average Python or Javascript user.
Well, yeah, but they generally don't like types at all. You won't have much knowledge to draw on if that's all you've ever done, unless you're learning another language in the same space with the same problems.
> it has widely used macros that obfuscate a lot of things that further adds to the complexity.
This is what's stumped me when learning Rust. It could be the resources I used, whixh introduced macros early on with no explanation.
Macros are introduced early in Rust for a couple reasons.
1. println!() is a macro, so if you want to print anything out you need to grapple with what that ! means, and why println needs to be a macro in Rust.
2. Macros are important in Rust, they're not a small or ancillary feature. They put a lot of work into the macro system, and all Rust devs should aspire to use and understand metaprogramming. It's not a language feature reserved for the upper echelon of internal Rust devs, but a feature everyone should get used to and use.
Hard disagree, macros almost always lead the "too clever for you own good" even when the macro system is safe. Macros should always be used sparingly, and I think that Rust teeters on the edge of encouraging too much complexity for the sake of convenience. As a systems programmer I am allergic to unnecessary levels of indirection that make it more difficult for me to reason about what the system is actually doing and how performance and hardware interaction will be affected.
At the same time, you don’t need to author macros. I have basically never written one, for example.
If a language needs an article like this, absolutely begging people to bite the bullet to learn it, maybe that's a language design smell.
Disclaimer: I haven't taken the time to learn Rust so maybe don't take this too seriously..
I don't know how to read your comment other than "nothing hard is worth doing". Some things have benefits and drawbacks, is the existence of drawbacks always a non-starter for you?
I'm trying to phrase this as delicately as I can but I am really puzzled.
If someone wrote an article about how playing the harp is difficult, just stick with it... would you also say that playing the harp is a terrible hobby?
Maybe people need persuading to learn Rust not just because they think it's hard, but also because they think it's bad? Not everything hard is worth doing. Difficulty is just one of the factors to consider.
I started to learn Rust, but I was put off by the heavy restrictions the language imposes and the attitude that this is the only safe way. There's a lack of acknowledgement, at least in beginner materials, that by choosing to write safe Rust you're sacrificing many perfectly good patterns that the compiler can't understand in exchange for safety. Eventually I decided to stop because I didn't like that tradeoff (and I didn't need it for my job or anything)
The thing is, if you want to learn Rust this site contains good advice on how to do it. I know, because I learned Rust.
Rust isn't a language you should pick up if you're not ready to put in the work. Just like you shouldn't go for full blown automotive grade C coding if you just want to learn coding quickly to get a job or something.
Rust has a steep learning curve, but the harder part (as mentioned in the article) is to unlearn patterns from other programming languages if you think you're already a good programmer.
I think one can understand Rust and still dislike it? Not every criticism of Rust comes from thinking it is too hard. I appreciate it for what it is and the problems it tries to solve. I just don't like many aspects of design of the language, seeing it as unnecessarily ugly for achieving it's aims.
> by choosing to write safe Rust you're sacrificing many perfectly good patterns that the compiler can't understand in exchange for safety
Historically, programmers drastically overestimate their ability to write perfectly safe code, so it's an enormous benefit if the compiler is able to understand whether it's actually safe.
The first part of your statement feels true, although that's... unverified and lacks actual backing up.
The second part of your statement is very debatable based on what safe means in this case, and whether it's an enormous benefit for a given situation.
There's plenty of stories [0][1] about Rust getting in the way and being very innappropriate for certain tasks and goals, and those "enormous benefits" can become "enormous roadblocks" in different perspectives and use cases.
In my personal and very subjective opinion I think Rust can be very good when applied to security applications, realtime with critical safety requirements (in some embedded scenarios for example), that sort of stuff. I think it really gets in the way too much in other scenarios with demanding rules and pattern that prevent from experimenting easily and exploring solutions quickly.
[0]https://barretts.club/posts/rust-for-the-engine/ [1]https://loglog.games/blog/leaving-rust-gamedev/
Let me help you
If it takes the average person 1 million hours to learn rust then the average person won't learn rust
If it takes the average person 1 hour to learn rust then the average person will learn rust.
If you were designing a language which would you pick all else being equal?
To your question, no but I wouldn't be puzzled when most people pick up a guitar. (Both are so much more intuitive than any programming language so the metaphor sets false expectations. Slick political move, but probably just turns more people off of Rust)
> If you were designing a language which would you pick all else being equal?
But why would you think all else is equal? You might not agree with the tradeoffs Rust makes, and it's not as if there's a perfect language for all uses, but it absolutely makes hard software easier to write.
I've had the opportunity to debug weird crazy memory corruption, as well as "wow it's hard to figure out how to design this in Rust", and having come to terms with things much like this blog post I now get more work done, with less corruption _and_ design problems.
The truth is that by the time you are a senior developer, you will have encountered the lessons that make rust worth learning but may not have truly understood all the implications.
Many people will think, I have a garbage collected language, rust has nothing to teach me. Even in garbage collected languages, people create immutable types because the possibility of shared references with mutability makes things incredibly chaotic that they look for immutability as a sort panacea. However, once you have immutable types you quickly realize that you also need ergonomic ways of modifying those objects, the methods you create to do so are often more cumbersome than what would be permitted for a mutable object. You wish there was some way to express, "There is a time where this object is mutable and then it becomes immutable." Enter the borrow checker.
Once you are borrow checking... why are you garbage collecting? Well, expressing those timelines of mutability and existence is a cost because you need to understand the timeline and most people would rather not spend that energy--maybe mutability or the poor ergonomics of immutable objects wasn't so bad. So, I garbage collect because I do not want to understand the lifetimes of my objects. Not understanding the lifetimes of objects is what makes shared mutability hard. Immutability eliminates that problem without requiring me to understand. Rust can teach this lesson to you so that you make an informed choice.
Of course, you can also just listen to me and learn the same lesson but there is value for many people to experience it.
Rust design decisions are pretty hard to understand sometimes, Mojo is another language with a borrow-checker but it is not nearly as hard to learn as Rust due to making a few decisions. First is value semantics, in Rust people are told to always clone when learning, why isn't this semantics built into the language? It is what you have in most static languages - C, C++, Go, etc. This is the mental model many people come to Rust with.
Secondary, Mojo's lifetime does not tell the compiler when a value is safe to use but when it is safe to delete, in this way the lifetime is not scope based, references will extend the lifetime of the value they reference, but values will be destroyed immediately after their last use. In Mojo you'll never see "value does not live long enough".
Just these two design decisions defines away so many ergonomic issues.
> people are told to always clone when learning, why isn't this semantics built into the language?
Because cloning as opposed to copying is expensive and it generates a new instance of a type. In C, you don't clone, you simply copy the struct or pointer, which will lead to a pointer to the same memory or a struct with members pointing to the same memory.
C++ on the other hand has a copy constructor, and you have to move explicitly, often generating unnecessary copies (in the sense of clone)
> Mojo's lifetime does not tell the compiler when a value is safe to use but when it is safe to delete,
What happens if you pass the variable mutably to a function?
> What happens if you pass the variable mutably to a function?
What happens in what manner? Mojo uses ASAP memory model, values will always be destroyed at the point of its last use. Mojo dataflow analysis will track this.
In terms of safety, Mojo will enforce `alias xor mutability` - like in Rust.
> C++ on the other hand has a copy constructor, and you have to move explicitly, often generating unnecessary copies (in the sense of clone)
Mojo also has copy and move constructors, but unlike in C++ these are not synthesised by default; the type creator has to either explicitly define the constructors or add a synthesiser. In Mojo, you can have types that are not copyable and not movable, these types can only be passed by reference. You can also have types that are copyable but not movable, or movable but not copyable.
> but values will be destroyed immediately after their last use
Is this reference counting?
Nah, deterministic compiler analysis. Something they call ASAP memory management
Learning any programming language at all feels 10x as hard to beginners, so you might as well say programming is not worth learning period in this case. Anything new has a learning curve to it.
I suspect an article like this says more about the author than the language.
Note I’m not being critical of the author here. I think it’s lovely to turn your passion into trying to help others learn.
The article focuses on the learning curve rather than the problem Rust is solving, as an observation. Think you need both of those to draw a conclusion as to whether it’s worth doing.
I think you can have a lot of debate on the design decisions on Rust, but I don't think the need for these articles tell you a lot about the language itself. I'd argue that Python needs articles like this more so than Rust does, but for entirely different reasons. In two decades of more and more programmers who aren't coming from an engineering background, I've yet to see anyone who used a Python generator or slots. Data Classes are less rare, but mainly in the form of pydantics "version". Which doesn't exactly matter for a lot of Python code... This is a world where 4chan can serve 4 million concurrent users an apache server running a 10k line PHP file neither of which have been updated since 2015... so you can be fine doing inefficient and entirely in-memory Python code 95% (or more) of the time.
That doesn't mean you should though. Imagine how much energy is being wasted globally on bad Python code... The difference is of course that anyone can write it, and not everyone can write Rust. I'm not personally a big fan of Rust, I'd chose Zig any day of the week... but then I'd also choose C over C++, and I frankly do when I optimise Python code that falls in those last 5%. From that perspective... of someone who really has to understand how Python works under the hood and when to do what, I'd argue that Rust is a much easier langauge to learn with a lot less "design smell". I suppose Python isn't the greatest example as even those of us who love it know that it's a horrible language. But I think it has quite clearly become the language of "everyone" and even more so in the age of LLM. Since our AI friends will not write optimised Python unless you specifically tell them to use things like generators and where to use them, and since you (not you personally) won't because you've never heard about a generator before, then our AI overlords won't actually help.
> maybe that's a language design smell
why
As someone who learned Rust, bur mostly uses Python in the day to day, I don't think Rust has a language design smell. It is just a very strict language with some of the strictness out there to ruin your day if you try to program Rust like it isn't Rust.
What that means is for example, if you have high aesthetical ideals and try to write object oriented code you will hit a brick wall eventually. Why? Notnbecause Ruwt is a bad language, but because you try to write Rust like it is Java or something.
Rust is a very nice language if you respect that there are Rust-ways of doing things that and that these ways are more data oriented than you might be used to.
The strictness can be daunting for beginners, but with increasing complexity it becones an absolute godsend. Where in other languages I find errors only when they happen, most Rust code just works (provided you write it in a Rust way), because the errors will caught during compilation.
That doesn't prevent logic errors, but these can be addressed with the absolute stellar test integrations. Now Rust is not all roses, but it is certainly a language worth learning even if you never use it. The ways it mitigates certain classes of errors can be turned into good coding practises for other languages as well.
Maybe Rust is so complex, it is even more complex for an LLM to generate correct code (one-shot) without hallucinating non-existent functions.
Would rather have that than all the issues that JavaScript or any other weakly typed and dynamically typed language.
There _are_ more than two programming languages, though. I feel like most of the debates about Rust devolve into the same false choice between safety and ease.
Before Rust I was hearing the same argument from Haskell or Scala developers trying to justify their language of choice.
I know Rust is here to stay, but I think it’s mostly because it has a viable ecosystem and quality developer tools. Its popularity is _in spite of_ many of its language features that trade that extra 1% of safety for 90% extra learning curve.
> features that trade that extra 1% of safety for 90% extra learning curve.
I remember both MS and goog having talks about real-world safety issues in the range of 50% of cases were caused by things that safe rust doesn't allow (use after free, dangling pointers, double free, etc). The fact that even goog uses it, while also developing go (another great language with great practical applications) is telling imo.
I have taken the time to learn rust and you're absolutely right. It's a very complex, design-by-committee language. It has brilliant tooling, and is still much less complex than it's design-by-committee competitor C++, but it will never be easy to learn.
> It's a very complex
I find it relatively simple. Much simpler than C++ (obviously). For someone who can write C++ and has some experience wth OCaml/Haskell/F#, it's not a hard language.
Sure, C++ has a more complex spec, nobody can argue against that.
Complex is the wrong word. Baffling is a better word. Or counterintuitive, or cumbersome. If “easy enough for someone with experience in C++, OCaml, Haskell, and F#” were the same thing as “not hard” then I don’t think this debate would come up so frequently.
What you call "baffling", I call "different". Being different doesn't mean it's "complex" or even "hard" (in isolation), but it can be baffling, in the same way that driving on the other side of the road for the first time can feel baffling (but doesn't mean it's "wrong").
Of course, this is very subjective. For someone who only knows python or javascript at a superficial level, Rust may seem out of reach. But if you're ok with the most common programming paradigms, I don't find Rust baffling.
I mean, you can't expect to learn a new language in a few days, it'll always take a bit of work. My feeling is that people complaining of the language being hard aren't putting the effort.
My experience is that Rust is a relatively small language which doesn't introduce a lot of new concepts. The syntax is quite intuitive, and the compiler super helpful. The borrower checker was the only new thing for me. I'm not an expert at all, but my experience is that after spending 2 weeks full-time reading books and experimenting, I was able to work professionally with the language without feeling too much friction.
On the other hand, after spending much more time on C++, I don't feel really comfortable with the language.
C++ is a huge and complex language. I worked in it, on and off, from 2002 through 2014 or so and never really felt comfortable, either. Everyone seems to use their own dialect.
(I'm working on learning Rust on my free time.)
There is a trade off. Rust gave us fast, and safe. It did not give us "easy to learn".
I think it is a very good example of why "design by committee" is good. The "Rust Committee" has done a fantastic job
Thank you
They say a camel is a horse designed by a committee (https://en.wiktionary.org/wiki/a_camel_is_a_horse_designed_b...)
Yes:
* Goes twice as far as a horse
* On half the food and a quarter the water of a horse
* Carries twice as much as a horse
Yes, I like design by committee. I have been on some very good, and some very bad committees, but there is nothing like the power of a good committee
Thank you Rust!
It's just a programming language, not a religion.
Well, it does look like there is a will to mimic religious social structure in the community, be it as a satiric form of it. I mean, I guess they purposefully named their pancakes cargo, as in "cargo cult", didn't they? Rustacean, rustomicon, and the other few words I saw leak out of the community all seem to go in the same spirit. I'm almost surprised they didn't went with more fancy terms for these core concepts of ownership and borrowing. Perl was also full of religious stuff like blessing your object, though Larry was actually more in the "true devot" side of the line.
the dogmatic culture would probably be my first suggestion. i always ask why are there any CVEs for rust if its "memory-safe" but never get an answer suprisingly
> i always ask why are there any CVEs for rust if its "memory-safe" but never get an answer suprisingly
The answer is straightforward: bugs exist. Even in formally proven software, mistakes can be made. Nothing is perfect.
Additionally, memory safety is a property that when people talk about it, they mean by default. All languages contain some amount of non-proven unsafe code in their implementation, or via features like FFI. Issues can arise when these two worlds interact. Yet, real-world usage shows that these cases are quite few compared to languages without these defaults. The exceptions are also a source of the CVEs you’re talking about.
CVE is not only for memory leak though, while eliminating (or even drastically reducing) such a class of issue is a fair point to advertise, it should not be confused as a magic safety facility that makes go away any security concern.
its not design by committee its design by Pull request It doesn't have a central https://en.wikipedia.org/wiki/Benevolent_dictator_for_life like python used to so people suggest and implement features as a group, with code counting for a lot (although theoretical issues with safety/design also matter) as opposed to companies arguing for their pet features endlessly without much difference. Look at how long it takes C++ to get any new features.
> Look at how long it takes C++ to get any new features.
I’m not sure “it doesn’t have enough features” has ever been anyone’s complaint about C++.
> If a language needs an article like this, absolutely begging people to bite the bullet to learn it, maybe that's a language design smell.
The problem with articles like this is that they don't really get to the heart of the problem:
There are programs that Rust will simply not let you write.
Rust has good reasons for this. However, this is fundamentally different from practically every programming language that people have likely used before where you can write the most egregious glop and get it to compile and sometimes even kinda-sorta run. You, as a programmer, have to make peace with not being able to write certain types of programs, or Rust is not your huckleberry.
> There are programs that Rust will simply not let you write.
Can you specify a few of these programs?
I can see where Rust might not allow you to write something the way you want to, but I fail to see how a program would not be expressible in rust...
They mean in Safe Rust. Unsafe is included in Rust for this reason.
Is safe rust not Turing complete? I can see the argument that a purist "safe rust only" program might be slow, but it still will be expressible
Turning completeness doesn’t take efficiency into account, nor the reality of things like “call into the operating system so that you can display output” that are necessary when building real systems.
> There are programs that Rust will simply not let you write.
If you're writing purely safe code, I will say this is true in a practical sense, but you can almost always use unsafe to write whatever you think rust won't let you do.
I'm not sure there are many cases where I would choose rust. I'm open to it. I just think in any given situation there would most likely be a better option.
Perhaps it will become prevalent enough that it will make sense in the future.
As systems programmer there are no better options. C is inherently unsafe, C++ is awful to work with and unsafe without careful use of pre-made safe abstractions that can't catch everything at runtime...
I have written C for decades and love the language, but Rust has convinced me that we need to evolve beyond the 70s. There's no excuse anymore.
It's the best fit I'm aware of for any task where correctness is worth the dev time hit. That includes systems programming tasks, where you're shuffling memory and files and system resources around and an error could kill your program or corrupt resources; or as a library for critical parts of your program, which could then provide an interface for other languages to use that allow for faster development & prototyping.
It's definitely the perfect language for writing a browser from scratch, as it was designed for almost 20 years ago. Of course nowadays it's already completely dominating that area, and their creator's "unix" has taken over the world and was not overtaken by something a random dude called ladybird.
"Safe" Rust is generally a simple language compared to C++. The borrow checker rules are clean and consistent. However, writing in it isn't as simple or intuitive as what we've seen in decades of popular systems languages. If your data structures have clear dependencies—like an acyclic graph then there's no problem. But writing performant self-referential data structures, for example, is far from easy compared to C++, C, Zig, etc.
On the opposite, "Unsafe" Rust is not simple at all, but without it, we can't write many programs. It's comparable to C, maybe even worse in some ways. It's easy to break rules (aliasing for exmaple). Raw pointer manipulation is less ergonomic than in C, C++, Zig, or Go. But raw pointers are one of the most important concepts in CS. This part is very important for learning; we can't just close our eyes to it.
And I'm not even talking about Rust's open problems, such as: thread_local (still questionable), custom allocators (still nightly), Polonius (nightly, hope it succeeds), panic handling (not acceptable in kernel-level code), and "pin", which seems like a workaround (hack) for async and self-referential issues caused by a lack of proper language design early on — many learners struggle with it.
Rust is a good language, no doubt. But it feels like a temporary step. The learning curve heavily depends on the kind of task you're trying to solve. Some things are super easy and straightforward, while others are very hard, and the eventual solutions are not as simple, intuitive or understandable compared to, for example, C++, C, Zig, etc.
Languages like Mojo, Carbon (I hope it succeeds), and maybe Zig (not sure yet) are learning from Rust and other languages. One of them might become the next major general-purpose systems language for the coming decades with a much more pleasant learning curve.
"raw pointers are one of the most important concepts in CS" that's a reach and a half, I don't remember the last time I've used one
The concept of being able to reference a raw memory address and then access the data at that location directly feels pretty basic computer science.
Perhaps you do software engineering in a given language/framework?
A clutch is fundamental to automotive engineering even if you don’t use one daily.
>and then access the data at that location
Or many other locations, by many other authors, at many other times, or simultaneously.
I think Java pointers wouldn't count as raw pointers despite having many similar characteristics.
One approach that I don't see often enough is to focus on learning a subset of the language first. For example, in my own book on Rust, I skip teaching lifetimes. It's not necessary to write functions with lifetimes to build quite a few fully functioning programs. The same with macros (although the fact that their signatures are opaque doesn't make it easy for beginners). On the other hand, I disagree with the advice to rely on copy() or clone() - it's better to learn about borrowing from the beginning since it's such a fundamental part of the language.
These replies mainly revealed to me the general response people have on being corrected. Its very easy to become a stubborn programmer after being in the industry for so long.
I'd advise these people to personally figure out why they're so against compiler suggestions. Do you want to do things differently? What part stops you from doing that?
Rust is wonderful but humbling!
It has a built in coach: the borrow checker!
Borrow checker wouldn't get off my damn case - errors after errors - so I gave in. I allowed it to teach me - compile error by compile error - the proper way to do a threadsafe shared-memory ringbuffer. I was convinced I knew. I didn't. C and C++ lack ownership semantics so their compilers can't coach you.
Everyone should learn Rust. You never know what you'll discover about yourself.
"Rust is wonderful but humbling!"
It's an abstraction and convenience to avoid fiddling with registers and memory and that at the lowest level.
Everyone might enjoy their computation platform of their choice in their own way. No need to require one way nor another. You might feel all fired up about a particular high level language that you think abstracts and deploys in a way you think is right. Not everyone does.
You don't need a programming language to discover yourself. If you become fixated on a particular language or paradigm then there is a good chance you have lost sight of how to deal with what needs dealing with.
You are simply stroking your tools, instead of using them properly.
@gerdesj your tone was unnecessarily rude and mean. Part of your message makes a valid point but it is hampered by unnecessary insults. I hope the rest of your day improves from here.
I don’t specifically like Rust itself. And one doesn’t need a programming language to discover themselves.
My experience learning Rust has been that it imposes enough constraints to teach me important lessons about correctness. Lots of people can learn more about correctness!
I’ll concede- “everyone” was too strong; I erred on the side of overly provocative.
It does not teach you any fundamental lessons about correctness. It teaches you lessons about correctness within the framework Rust imposes; that's all
Wow who pissed in your coffee? he likes rust ok?
And he's telling other people they should like it as well, because he has seen the light.
My gut feeling says that there's a fair bit of Stockholm Syndrome involved in the attachments people form with Rust.
You could see similar behavioral issues with C++ back in the days, but Rust takes it to another level.
I think most of us enamoured with rust are c++ refugees glad the pain is lessened. The tooling including the compiler errors really are great though. I like the simplicity of c, but I would still pick rust for any new project just for the crates and knowing I'll never have to debug a segfault. I like pytorch and matlab fine for prototyping. Not much use for in-between languages like go or c# but I like the ergonomics of them just fine. I don't think it is at all weird for people coming from c++ or even c to like rust and prefer it over those other languages. We have already paid the cost of admission, and it comes with real benefits.
> You could see similar behavioural issues with C++ back in the days
I think that it's happened to some degree for almost every computer programming language for a whiles now - first was the C guys enamoured with their NOT Pascal/Fortran/ASM, then came the C++ guys, then Java, Perl, PHP, Python, Ruby, Javascript/Node, Go, and now Rust.
The vibe coding people seem to be the ones that are usurping Rust's fan boi noise at the moment - every other blog is telling people how great the tool is, or how terrible it is.
> Everyone should learn Rust.
I know this feels like a positive vibe post and I don’t want to yuck anyone’s yum, but speaking for myself when someone tells me “everyone should” do anything, alarm bells sound off in my mind, especially when it comes to programming languages.
I think everyone should learn many different programming languages, because being exposed to different paradigms helps develop programming skill.
Yeah I agree, I enjoy the process. I don’t think that’s what’s behind “everyone should learn rust” in this case, and many cases. It feels like a “cause”.
The compilers maybe not, but static analysers already go a long way, it is a pity that it is still a quixotic battle to make developers adopt them, even if it isn't 100% all the way there.
If it isn't the always hated SecDevOps group of people pushing for the security tooling developers don't care about, at very least on build pipelines, they would keep collecting digital dust.
Got recommended learning paths? I tend to prefer follow along adventures via video.
Check out Jon Gjengset.
https://www.youtube.com/@jonhoo
I wouldn't agree with that. Jon's content is great, but it's really not aimed at beginners, and some of his stuff really gets into the weeds.
The only way I think I’ll learn rust is if there’s a surge of job opportunities paying 300 K and up which require it.
The potential is definitely there, it looks like it might compete with C++ in the quant .
But we already have ocaml . From Jane Street, at least for me if you’re going to tell me, it’s time to learn an extremely difficult programming language, I need to see the money.
So far my highest paid programming job was in Python
My problem with rust is not the learning curve, but the absolute ugliness of the syntax. It's like Perl and C++ template metaprogramming had a child. I just can't stand it.
Python is my favourite, C is elegance in simplicity and Go is tolerable.
C may be simple, but its too simple to be called elegant. The lack of namespacing comes to mind. Or that it is a staticly typed language, whose type system is barely enforced (you have to declare all types, but sometimes it feels like everything decays to int and *void without the right compiler incantations). Or the build system, where you have to learn a separate language to generate a separate language to compile the program (which a both also not really simple and elegant in my eyes). Or null-terminated strings: to save some 7 bytes per string (on modern platforms) C uses one of the most dangerous and unelegant constructs in the popular part of the programming-world. Or the absolutely inelegant error handling, where you either return an in-band-error-value, set a global variable or both or just silently fail. Or the standard-library, that is littered with dangerous functions. Or the reliance of the language definition on undefined behaviour, that forces you to read a 700-page, expensive document back to back to know whether a vital check in your program might be ignored by compilers or when your program might shred your hard drive, despite you never instructing it to do so. Or...
C has a simple syntax, but it is most certainly not elegant.
C is elegant because as an extremely powerful programming language used to create an uncountable number of high-profile projects it's simple enough that I feel optimistic I could write a C compiler myself if it was really necessary.
It may be impractical for some tasks but the power:complexity rate is very impressive. Lua feels similar in that regard.
The mechanism for sharing definitions, 'include' aka "copy file here", is absolutely not elegant. The absolute minimum you could do maybe, but not elegant.
Not to mention the whacky preprocessor as a whole...
I would say includes/standard library/compilers going crazy on UB is part of the infrastructure or ecosystem around the language and not the language itself. And I agree absolutely, while C the language is beautiful, the infra around it is atrocious.
Have you had a look at Nim? I think you may like it.
Feels like a cult manual. "Do all things our way! Don't question anything!"
If you’re not prepared to be humble when you learn, you’re not actually trying to learn. (Or not trying very hard anyway),
There’s a difference between “do it like this” and “keep an open mind when you do this”.
You can learn rust any way you want; this is just a guide for how to learn it effectively.
A fairer comparison would be learning Japanese by going to Japan and insisting on speaking English except in Japanese language classes.
Yes, you can do that.
…but it is not the most effective way to learn.
The best way to learn is full immersion. It’s just harder.
If you don’t want that, don’t do it. It’s not a cult. That’s just lazy flippant anti-rust sentiment.
That's how every student forced to take a programming elective feels about any programming language.
>For instance, why do you have to call to_string() on a thing that’s already a string?
It's so hard for me to take Rust seriously when I have to find out answers to unintuitive question like this
Python community famously learned the hard way that sometimes the programmer needs to know that there are multiple kinds of string.
Personally, I’ve been using to_owned instead. Some of the people looking at my code don’t write rust, and I figure it makes things a bit easier to understand.
I’m not sure why it’s counterintuitive that &str and String are different things. Do you also find it counterintuitive in C++ that std::string is different from const char* ? What about &[u8] and Vec<u8> ?
Better analogy is std::string_view vs std::string
Nah. &str is const char* exactly. It's as primitive as types in rust get.
Nope. `&str` includes the length of the slice, which `const char*` does not. `std::string_view` is the proper analogy.
Strings are like time objects: most people and languages only ever deal with simplified versions of them that skip a lot of edge cases around how they work.
Unfortunately going from most languages to Rust forces you to speedrun this transition.
Just because a language is not high level enough to have a unique concept of "string" type doesn't mean you shouldn't take it seriously.
Even very high-level languages don't have singular concepts of string. Every serious language I can think of differentiates between:
- A sequence of arbitrary bytes
- A sequence of non-null bytes interpreted as ASCII
- A sequence of unicode code points, in multiple possible encodings
On the other side where this question is not asked we have things like
And it's utter madness that everyone does anything important with languages like that.The question is worded weird for fun. One is a string slice (like char*) and one is String or &String, which is closer to an object.
C++ is a horribly cmplicated language, sometimes I have to cast something to an int when it's already an integer. /s
I have a hard time understanding why people have such a hard time accepting that you need to convert between different text representations when it's perfectly accepted for numbers.
A learning curve measures time on the x axis and progress on the y axis.
A flat learning curve means you never learn anything :-\
You may be able to draw one that way but it completely neglects the way people use the term ordinarily “a steep learning curve” is not an easy to learn thing.
In point of fact, I think the intended chart of the idiom is effort (y axis) to reach a given degree of mastery (x axis)
I don't think the idiom has in mind any particular curve. I think it's just another case of a misuse becoming idiomatic without any meaning beyond the phrase taken as a unit. E.g.
- another think coming -> another thing coming
- couldn't care less -> could care less
- the proof of the pudding is in the eating -> the proof is in the pudding
It's usually not useful to try to determine the meaning of the phrases on the right because they don't have any. What does it mean for proof to be in a pudding for example?
The idiom itself is fine, it's just a black box that compares learning something hard to climbing a mountain. But learning curves are real things that are still used daily so I just thought it was funny to talk as if a flat one was desirable.
(not related to your overall point)
> - another think coming -> another thing coming
Fascinating. I had never come across this before. I've only ever seen people use "another thing coming".
https://en.m.wikipedia.org/wiki/Learning_curve
"Flattening the derivative of Rust's learning curve" really doesn't roll off the tongue though
A steep line still has a flat derivative
Yeah that's true. But it would be on brand for a post that emphasizes the importance of accuracy and attention to detail.
It should be called "learning hill" instead.
People (colloquially) use phrases like "steep learning curve" because they imagine learning curve is something you climb up, a.k.a. a hill.
This is incorrect. A learning curve measures expertise on the x axis and effort on the y axis. Hence the saying "steep learning curve".
Calling it inaccurate was too harsh; my definition only became common usage in 1970, and the original “time vs learning” is still used in academic circles.
https://en.wikipedia.org/wiki/Learning_curve
It is unclear how this comment was meant; in any case, it is appreciated. As stated in the link:
“The common English usage aligns with a metaphorical interpretation of the learning curve as a hill to climb.”
Followed by a graph plotting x “experience” against y “learning.”
That’s interesting. I always intuitively assumed x-axis was progress and y-axis was cumulative effort.
But flattening the learning curve doesn't have to mean to make it completely flat, just making it less steep :)
What we want is an "effort/difficulty curve" that measures how difficult something typically is over time from introduction to proficiency
It also could mean you don't need to learn beyond a certain point.
Ownership and lifetimes are arguably not about Rust, but about how we construct programs today. Big, interdependent, object graphs are not a good way of constructing programs in Rust or C++.
"You will have a much better time if you re-read your code to fix stupid typos before pressing “compile.”"
This is a strange one - I thought the rust compiler had famously helpful error messages, so why would I want to pore over my code looking for stupid typos when I can let the compiler find them for me? I am guaranteed to make stupid typos and want the computer to help me fix them.
cargo fix can automatically fix some, but not all, issues.
The whole learning curve seems overblown to me. You don’t need to grok every part of the language to start using it and being productive.
I thought it was quite manageable at beginner level…though I haven’t dived into async which I gather is a whole different level of pain
Async and the "function color" "problem" fall away if your entire app is in an async runtime.
Almost 90% of the Rust I write these days is async. I avoid non-async / blocking libraries where possible.
I think this whole issue is overblown.
Agreed. Function coloring is a solution (not a problem), one that's better than the alternatives.
The "function coloring problem" people are harming entire ecosystems. In JS for example there are very popular frameworks thay choose to wrap async in sync execution by throwing when encountering async values and re-running parts of the program when the values resolve. The crazy part with these solutions trying to remove coloring, is they don't, they hide it (poorly). So instead of knowing what parts of a program are async you have no idea.
"Just write everything async" is not remotely a good solution to the problem. Not everything needs to be async (in fact most things don't), and it's much harder to reason about async code. The issue is very much not overblown.
Why is async code harder to reason about? I've been using it in C# and the entire point is that it lets you write callbacks in a way that appears nearly identical to synchronous code. If you dive into concurrency (which is a separate thing but can be utilized with async code, such as joining multiple futures at the same time), that parts hard whether you're doing it with async or with explicit threads.
> I've been using it in C#
One reason why async-await is trivial in .NET is garbage collector. C# rewrites async functions into a state machine, typically heap allocated. Garbage collector automagically manages lifetimes of method arguments and local variables. When awaiting async functions from other async functions, the runtime does that for multiple async frames at once but it’s fine with that, just a normal object graph. Another reason, the runtime support for all that stuff is integrated into the language, standard library, and most other parts of the ecosystem.
Rust is very different. Concurrency runtime is not part of the language, the standard library defined bare minimum, essentially just the APIs. The concurrency runtime is implemented by “Tokio” external library. Rust doesn’t have a GC; instead, it has a borrow checker who insists on exactly one owner of every object at all times, makes all memory allocations explicit, and exposed all these details to programmer in the type system.
These factors make async Rust even harder to use than normal Rust.
None of this is scary.
> The concurrency runtime is implemented by “Tokio” external library.
Scare quotes around Tokio?
You can't use Rails without Rails or Django without Django.
The reason Rust keeps this externally is because they didn't want to bake premature decisions into the language. Like PHP's eternally backwards string library functions or Python's bloated "batteries included" standard library chock full of four different XML libraries and other cruft.
> instead, it has a borrow checker who insists on exactly one owner of every object at all times, makes all memory allocations explicit, and exposed all these details to programmer in the type system
Table stakes. Everyone knows this. It isn't hard or scary, it just takes a little bit of getting used to. Like a student learning programming for the first time. It's not even that hard. Anyone can learn it.
It's funny people complain about something so easy. After you learn to ride the bike, you don't complain about learning to ride the bike anymore.
> Rust is very different.
Oh no!
Seriously this is 2025. I can write async Rust without breaking a sweat. This is all being written by people who don't touch the language.
Rust is not hard. Stop this ridiculous meme. It's quite an easy language once you sit down and learn it.
> Why is async code harder to reason about?
I don't know about C#, but at least in Rust, one reason is that normal (non-async) functions have the property that they will run until they return, they panic, or the program terminates. I.e. once you enter a function it will run to completion unless it runs "forever" or something unusual happens. This is not the case with async functions -- the code calling the async function can just drop the future it corresponds to, causing it to disappear into the ether and never be polled again.
Because 1 mistake somewhere will make your whole application stuck and then you must locate which call is blocking.
Async's issue is not coloring. It's introducing issues that just don't exist in sync code like pinning, synchronization, future lifetimes...
Coloring just exacerbates the issues because it's viral, not because coloring itself is an issue.
That’s not a solution to the coloring problem any more than making everything red was in 2015 (ie, all the tradeoffs mentioned in the article [0] still apply).
[0]: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
It’s completely overblown. Almost every language with async has the same “problem”.
I’m not calling this the pinnacle of async design, but it’s extremely familiar and is pretty good now. I also prefer to write as much async as possible.
The "function color is a problem" people invented a construct that amplifies the seriousness. It's not really a big deal.
My biggest issue with the whole "function colour" thing is that many functions have different colours. Like, these two:
I can't just treat `bar` the same as `foo` because it doesn't give me a String, it might have failed to give me a String. So I need to give it special handling to get a String. This also doesn't give me a String. It gives me a thing that can give me a String (an `impl Future<Output=String>`, to be more specific), and I need to give it special handling to get a String.All of these function have different colours, and I don't really see why it's suddenly a big issue for `qux` when it wasn't for `bar`.
Excellent retort!
How are async closures / closure types, especially WRT future pinning?
Async closures landed in stable recently and have been a nice QoL improvement, although I had gotten used to working around their absence well enough previously that they haven’t been revolutionary yet from the like “enabling new architectural patterns” perspective or anything like that.
I very rarely have to care about future pinning, mostly just to call the pin macro when working with streams sometimes.
While I'd like to have it, it doesn't stop me from writing a great deal of production code without those niceties.
When it came time for me to undo all the async-trait library hack stuff I wrote after the feature landed in stable, I realized I wasn't really held back by not having it.
Is there a concise document that explains major decisions behind Rust language design for those who know C++? Not a newbie tutorial, just straight to the point: why in-place mutability instead of other options, why encourage stack allocation, what problems with C++ does it solve and at what cost, etc.
I’m not aware of one, but I’m happy to answer questions.
> in-place mutability
I’m not sure what this means as.
> why encourage stack allocation
This is the same as C++, things are stack allocated by default and only put on the heap if you request it. Control is imporrant
> what problems with C++ does it solve and at what cost
The big one here is memory safety by default. You cannot have dangling pointers, iterator invalidation, and the like. The cost is that this is done via compile time checks, and you have to learn how to structure code in a way that demonstrates to the compiler that these properties are correct. That takes some time, and is the difficulty people talk about.
Rust also flips a lot of defaults that makes the language simpler. For example, in C++ terms, everything is trivially relocatable, which means Rust can move by default, and decided to eliminate move constructors. Technically Rust has no constructors at all, meaning there’s no rule of 3 or 5. The feeling of Rust code ends up being different than C++ code, as it’s sort of like “what if Modern C++ but with even more functional influence and barely any OOP.”
Rust has better defaults for types than C++, largely because the C++ defaults came from C. Rust is more ergonomic in this regard. If you designed C++ today, it would likely adopt many of these defaults.
However, for high-performance systems software specifically, objects often have intrinsically ambiguous ownership and lifetimes that are only resolvable at runtime. Rust has a pretty rigid view of such things. In these cases C++ is much more ergonomic because objects with these properties are essentially outside the Rust model.
In my own mental model, Rust is what Java maybe should have been. It makes too many compromises for low-level systems code such that it has poor ergonomics for that use case.
Interestingly, CPU-bound high-performance systems are also incompatible with Rust’s model. Ownership for them is unambiguous, but Rust has another issue, doesn’t support multiple writeable references of the same memory accessed by multiple CPU cores in parallel.
A trivial example is multiplication of large square matrices. An implementation needs to leverage all available CPU cores, and a traditional way to do that you’ll find in many BLAS libraries – compute different tiles of the output matrix on different CPU cores. A tile is not a continuous slice of memory, it’s a rectangular segment of a dense 2D array. Storing different tiles of the same matrix in parallel is trivial in C++, very hard in Rust.
That's the tyranny of Gödel incompleteness (or maybe Rice's theorem, or even both): useful formal systems can be either sound or complete. Rust makes the choice of being sound, with the price of course being that some valid operations not being expressible in the language. C of course works the other way around; all valid programs can be expressed, but there's no (general) way to distinguish invalid programs from valid programs.
For your concrete example of subdividing matrixes, that seems like it should be fairly straightforward in Rust too, if you convert your mutable reference to the data into a pointer, wrap your pointer arithmetic shenanigans in an unsafe block and add a comment at the top saying more or less "this is safe because the different subprograms are always operating on disjoint subsets of the data, and therefore no mutable aliasing can occur"?
Hard in safe rust. you can just use unsafe in that one area and still benefit in most of your application from safe rust.
I don’t use C++ for most of my applications. I only use C++ to build DLLs which implement CPU-bound performance sensitive numeric stuff, and sometimes to consume C++ APIs and third-party libraries.
Most of my applications are written in C#.
C# provides memory safety guarantees very comparable to Rust, other safety guarantees are better (an example is compiler option to convert integer overflows into runtime exceptions), is a higher level language, great and feature-rich standard library, even large projects compile in a few seconds, usable async IO, good quality GUI frameworks… Replacing C# with Rust would not be a benefit.
I would definitely rather use C# or java in a GUI app, yes.
It does sound like quite a similar model; unsafe Rust in self contained regions, safe in the majority of areas.
FWIW in the case where you're not separating code via a dynamic library boundary, you give the compiler an opportunity to optimise across those unsafe usages, e.g. inlining opportunities for the unsafe code into callers.
Java should have been like Modula-3, Eiffel, Active Oberon, unfortunately it did not and has been catching up to rethink its design while preserving its ABI.
Thankfully C# has mostly catched up with those languages, as the other language I enjoy using.
After that, is the usual human factor on programming languages adoption.
> However, for high-performance systems software specifically, objects often have intrinsically ambiguous ownership
What is the evidence for this? Plenty of high-performance systems software (browsers, kernels, web servers, you name it) has been written in Rust. Also Rust does support runtime borrow-checking with Rc<RefCell<_>>. It's just less ergonomic than references, but it works just fine.
Anyone that works on e.g. database kernels that do direct DMA (i.e. all the high-performance ones) experiences this. The silicon doesn’t care about your programming language’s ownership model and will violate it at will. You can’t fix it in the language, you have to accept the behavior of the silicon. Lifetimes are intrinsically ambiguous because objects have neither a consistent nor persistent memory address, a pretty standard property in databases, and a mandatory property of large databases. Yes, you can kind of work around it in idiomatic Rust but performance will not be anything like comparable if you do. You have to embrace the nature of the thing.
The near impossibility of building a competitive high-performance I/O scheduler in safe Rust is almost a trope at this point in serious performance-engineering circles.
To be clear, C++ is not exactly comfortable with this either but it acknowledges that these cases exist and provides tools to manage it. Rust, not so much.
New DB's like Tigerbeetle are written in Zig. Memory control was one of the prime reasons. Rust's custom allocators for the standard library have been a WIP for a decade now.
I think the major decisions behind Rust is being explicit and making the programmer make decisions. No NULLs, no Implicit conversions, no dangling pointers. Lifetimes, Optional, Results, each Match branch needs to exist, etc.
Side note: Stack allocation is faster to execute as there's a higher probability of it being cached.
Here is a free book for a C++ to Rust explanation. https://vnduongthanhtung.gitbooks.io/migrate-from-c-to-rust/...
> being explicit and making the programmer make decisions
Why RAII then?
> C++ to Rust explanation
I've seen this one. It is very newbie oriented, filled with trivial examples and doesn't even have Rust refs to C++ smart pointers comparison table.
I would say that RAII is very explicit: Resource Acquisition Is Initialization. When you initialize the struct representing the resource you are acquiring the resource. If you have a struct representing a resource you have the resource. Knowing this, you are also acquiring a call to drop when it goes out of scope. I would argue that the difference here isn't explicitness.
Instead, I would argue that rust is favoring a form of explicitness together with correctness. You have to clean up that resource. I have seen arguments that you should be allowed to leak resources, and I am sympathetic, but if we agree on explicitness as a goal then perhaps you might understand the perspective that a leak should be explicit and not implicit in a the lack of a call a some method. Since linear types are difficult to implement auto-drops are easier if you favor easily doing the correct thing. If you want to leak your resource, stash it in some leak list or unsafe erase it. That is the thing that should be explicit: the unusual choice, not all choices and not the usual choice alone.
But yeah, the drop being implicit in the explicit initialization does lead to developers ignoring it just like a leak being implicit if you forget to call a function often leads to unintentionally buggy programs. So when a function call ends they won't realize that a large number of objects are about to get dropped.
To answer your original question, the rationale is not in one concise location but is spread throughout the various RFCs that lead to the language features.
>> being explicit and making the programmer make decisions
>Why RAII then?
Their quote is probably better rephrased as _being explicit and making the programmer make decisions when the compiler's decision might impact safety_
Implicit conversion between primitives may impact the safety of your application. Implicit memory management and initialization is something the compiler can do safely and is central to Rust's safety story.
Rust is RAII at the compiler level. It's the language spec itself. That's probably the best way to describe the design and intent of Rust's memory model.
When you create a thing, you allocate it. That thing owns it and destroys it, unless you pass that ownership onto something else (which C++ RAII doesn't do very cleanly like Rust can).
Then it does some other nice things to reduce every sharp edge it can:
- No nulls, no exceptions. Really good Option<T> and Result<T,E> that make everything explicit and ensure it gets handled. Wonderful syntactic sugar to make it easy. If you ever wondered if your function should return an error code, set an error reference, throw an exception - that's never a design consideration anymore. Rust has the very best solution in the business. And it does it with rock solid safety.
- Checks how you pass memory between threads with a couple of traits (Send, Sync). If your types don't implement those (usually with atomics and locks), then your code won't pass the complier checks. So multithreaded code becomes provably safe at compile time to a large degree. It won't stop you from deadlocking if you do something silly, but it'll solve 99% of the problems.
- Traits are nicer than classes. You can still accomplish everything you can with classic classes, but you can also do more composition-based inheritance that classes don't give you by bolting traits onto anything you want.
- Rust's standard library (which you don't have to use if you're doing embedded work) has some of the nicest data structures, algorithms, OS primitives, I/O, filesystem, etc. of any language. It's had 40 years of mistakes to learn from and has some really great stuff in it. It's all wonderfully cross-platform too. I frequently write code for Windows, Mac, and Linux and it all just works out of the box. Porting is never an issue.
- Rust's functional programming idioms are super concise and easy to read. The syntax isn't terse.
- Cargo is the best package manager on the planet right now. You can easily import a whole host of library functionality, and the management of those libraries and their features is a breeze. It takes all of sixty seconds to find something you want and bring it into your codebase.
- You almost never need to think about system libraries and linking. No Makefiles, no Cmake, none of that build complexity or garbage. The compiler and cargo do all of the lifting for you. It's as easy as python. You never have to think about it.
This might not be exactly what you're looking for, but I really like "References are like jumps": https://without.boats/blog/references-are-like-jumps/
In-place mutability and stack allocation are for speed. This made them have variables and values inside them both as separate first-class citizens. The entire borrow checker is just for tracking variables access so that you don't need to clone values. I'd say that given this one guiding decision there are very little arbitrary ones in Rust. The rest of it pretty much had to be made exactly the way it is. Rust is more of a discovery than a construct.
C++ is just Rust without any attempt at tracking variable access and cloning which leads to a mess because people are too terrible at that to do that manually and ad-hoc. So Rust fixes that.
This reads like a list of symptoms of what's wrong with the ergonomics of Rust. This is not to bash Rust. It has its uses. But you need to balance what you are sacrificing for what you are getting.
Write a CHIP8 emulator!
Bonus: do it with no heap allocation. This actually makes it easier because you basically don’t deal with lifetimes. You just have a state object that you pass to your input system, then your guest cpu system, then your renderer, and repeat.
And I mean… look just how incredibly well a match expression works for opcode handling: https://github.com/ablakey/chip8/blob/15ce094a1d9de314862abb...
My second (and final) rust project was a gameboy emulator that basically worked the same way.
But one of the best things about learning by writing an emulator is that there’s enough repetition you begin looking for abstractions and learn about macros and such, all out of self discovery and necessity.
I’ve found emulators to be a pretty poor first project for rust specifically for the reasons you alluded to: That you need to know to write it without heap allocation (or other hoop jumping so long as you avoid juggling lifetimes) when so much literature and example emulator code doesn’t do this is a recipe for a bad experience. Ask me how I know.
If you’re going to write an emulator in this style, why even use an imperative language when something like Haskell is designed for this sort of thing?
These emulators already exist in basically every language, so why do anything? The point is the journey, which doesn’t need to be the shortest, most optimal path possible.
I’m saying it’s not optimal for learning the language, not that it’s not worth doing. I’ve worked on 3 different emulators for fun over the last few years, my first in rust. It was a bad experience for learning rust because I was following prior art which relied heavily on shared data structures and lots of poking randomly at blocks of RAM, a very natural way to think when you’re engrossed in the mechanics of an 8-bit CPU.
I had a better time writing a raycaster and later a path tracer, although by then I had learned to avoid dealing with the borrow checker…
Good article, thoughtful and well-written and (idioms aside) much of it applicable to learning other languages.
What is it that make that rust is said to have steep learning curve compared to other programing languages (in the same category)?
It doesn’t. The only language I know of in the same category as Rust is C++, which is much harder to learn and use.
Mostly borrow checker.
Lifetime syntax can be off putting.
Sorry, but it's easier to learn basic C++/C, and let beginner developers get lessons from a basic linter or clang-tidy for about the same result, for a fraction of the developer cost.
I want rust to be adopted and I believe companies should force it, but you will not get adoption from young developers and even less from senior C++ developers.
Not to mention rewriting existing C++ code in rust, which cost would be astronomical, although I do believe companies should invest in rewriting things in rust because it's the right thing to do.
Traits are yet another way of doing the CS concept of interfaces, regardless of the author's opinion.
Polymorphic dispatch on a set of known operations, that compose a specific type.
Regarding the first example, the longest() function, why couldn't the compiler figure it out itself? What is the design flaw?
You’re passing in two references and returning a reference.
The compiler knows the returned reference must be tied to one of the incoming references (since you cannot return a reference to something created within the function, and all inputs are references, the output must therefore be referencing the input). But the compiler can’t know which reference the result comes from unless you tell it.
Theoretically it could tell by introspecting the function body, but the compiler only works on signatures, so the annotation must be added to the function signature to let it determine the expected lifetime of the returned reference.
> Theoretically it could tell by introspecting the function body, but the compiler only works on signatures
Note that this is an intentional choice rather than a limitation, because if the compiler analyzed the function body to determine lifetimes of parameters and return values, then changing the body of a function could be a non-obvious breaking API change. If lifetimes are only dependent on the signature, then its explicit what promises you are or are not making to callers of a function about object lifetimes, and changing those promises must be done intentionally by changing the signature rather than implicitly.
> changing the body of a function could be a non-obvious breaking API change
This. Many trival changes breaks API. This is not ideal for library developers.
You can argue it is broken already, but this is forcing the breakage onto every api caller, not just some broken caller.
Oh yes, I didn’t mean to make it sound like a problem. I personally strongly prefer signature-based typing vs like in Typescript where you can easily be returning an entirely unintentional type and not realize it until you try to use it in an explicitly typed context down the line.
I also imagine it’s much faster for the type-checking pass of the compiler to just look at the signatures.
Compiler can figure that out, but the thing is compiler needs also to understand lifetimes at the site where this function is called. In general case compiler will not look into the code of a called function to see what it does, compiler relies on a function declaration.
That `longest` if defined without explicit lifetimes treated like a lifetime of a return value is the same as of the first argument. It is a rule "lifetime elision", which allows to not write lifetimes explicitly in most cases.
But `longest` can return a second reference also. With added lifetimes the header of the function says exactly that: the lifetime of a return value is a minimum of lifetimes of arguments. Not the lifetime of the first one.
It's a design choice.
To make a compiler automatically handle all of the cases like that, you will need to do an extensive static analysis, which would make compiling take forever.
Would be nice if an IDE can autofix it.
Maybe autofix as we type, or autofix when it save the document / advance to next line.
> Use String and clone() and unwrap generously; you can always refactor later
At that point you might as well be writing Java or Go or whatever though. GC runtimes tend actually to be significantly faster for this kind of code, since they can avoid all those copies by sharing the underlying resource. By the same logic, you can always refactor the performance-critical stuff via your FFI of choice.
So long as you're aware that you're not optimizing, it's fine. Trying to build something useful as a new Rust dev while worrying about lifetimes is going to be quite challenging, unless your intention is to specifically learn about lifetimes and the borrow checker.
Yes the borrow checker is central to Rust, but there are other features to the language that people _also_ need to learn and explore to be productive. Some of these features may attract them to Rust (like pattern matching / traits / etc.)
> At that point you might as well be writing Java or Go or whatever though.
And miss Option, Result, proper enums, powerful pattern matching, exhaustive pattern matching, affine types, traits, doctests... and the many other QoL features that I sorely miss when I drop to e.g. TS/Node.
I'm not using Rust for the borrow checker, but it's nice to have when I need it to hold my hand and not that much of an issue when I don't. I wanted to like Go but I just can't.
Dropping to no_std though... that was a traumatic experience.
>significantly faster for this kind of code
"Significantly" and "this kind" are load bearing sentences here. In applications where predictable latency is desired, cloning is better than GC.
This is also the baby steps of learning the language. As a programmer gets better they will recognize when they are making superflous clones. Refactoring performance-critical stuff in FFI, however, is painful and wont get easier with time.
Furthermore, in real applications, this only really applies to Strings and vectors. In most of my applications most `clones` are of reference types - which is only marginally more expensive than memory sharing under a GC.
I went through this the first year that I did Advent of Code in rust, like okay I read in all the strings from the input file and now they're in a vector, so I'm going to iterate the vector and add references to those strings into this other structure, but of course they're still owned by the original vector, that's awkward. Oh wait I can iter_into and then I get owned objects and that ownership can be transferred to the other structure instead, but now I need them to also be keys in a map, do I use references for that too?
Cloning small objects is lightning fast, turns out in a lot of these cases it makes sense to just do the clone, especially when it's a first pass. The nice thing is that at least rust makes you explicitly clone() so you're aware when it's happening, vs other languages where it's easy to lose track of what is and isn't costing you memory. So you can see that it's happening, you can reason about it, and once the bones of the algorithm are in place, you can say "okay, yes, this is what should ultimately own this data, and here's the path it's going to take to get there, and these other usages will be references or clones.
> Cloning small objects is lightning fast
It's really not, it's the way python works. Heap allocations are "fast" on modern CPUs that are too fast to measure for most stuff, but they're much (much) slower than the function call and code you're going to use to operate on whatever the thing it was you cloned.
Code that needs memory safety and can handle performance requirements like this has many options for source language, almost none of which require blog posts to "flatten the learning curve".
(And to repeat: it's much slower than a GC which doesn't have to make the clone at all. Writing Rust that is "Slower Than Java" is IMHO completely missing the point. Java is boring as dirt, but super easy!)
Cloning doesn’t imply heap allocation. Depends on the type.
If the object had a stack-bounded lifetime, the borrow checker would have been able to prove the analysis though. The advice is to clone things it can't, which pretty much requires that it go into the general heap. I'm sure there are some interesting counterexamples, but the situation you're imagining seems kinda academic.
Not to mention that even though you can always refactor later, will you really? It’s much easier not to.
In my experience, hobbyist Rust projects end up using unwrap and panic all over the place, and it’s a giant mess that nobody will ever refactor.
Compared to other programming languages, Rust's compiler and linters go a long way to implement best practices at build time.
> …. For instance, “a trait is a bit like an interface” is wrong, …
Wait. What's wrong with this statement?
I think this is just a mistake, in that “a bit like” is correct. The ways in which it are different depend on which language you are taking the concept of “interface” from, but the statement that it’s like one is accurate.
That makes sense.
Surrender! to compile
Weather the ferocious storm
You will find, true bliss
For me the most important thing about Rust is to understand that it's a langue with value semantics. Which makes it completely different than every mainstream language you encountered so far.
Variable in rust is not a label you can pass around and reuse freely. It's a fixed size physical memory that values can be moved into or moved out of. Once you understand that everything makes sense. The move semantics, cloning, borrowing, Sized, impl. Every language design element of rust is a direct consequence of that. It's the values that get created, destroyed and moved around and variables are actual first-class places to keep them with their own identity separate from values that occupy them. It's hard to notice this because Rust does a lot to pretend it's a "normal" language to draw people in. But for anyone with experience in programming that attempts to learn Rust I think this realization could make the process at least few times easier.
It's hard to shift to this new paradigm and embrace it, so in the meantime feel use a lot of Rc<> and cloning if you just need to bang out some programs like you would in any other mainstream language.
> Treat the borrow checker as a co-author, not an adversary
Why would I pair-program with someone who doesn’t understand doubly-linked lists?
For people who don't get the reference, this might be referring to the notoriously gnarly task of implementing a doubly-linked lists in Rust [1]
It is doable, just not as easy as in other languages because a production-grade linked-list is unsafe because Rust's ownership model fundamentally conflicts with the doubly-linked structure. Each node in a doubly-linked list needs to point to both its next and previous nodes, but Rust's ownership rules don't easily allow for multiple owners of the same data or circular references.
You can implement one in safe Rust using Rc<RefCell<Node>> (reference counting with interior mutability), but that adds runtime overhead and isn't as performant. Or you can use raw pointers with unsafe code, which is what most production implementations do, including the standard library's LinkedList.
https://rust-unofficial.github.io/too-many-lists/
Rust still needs a way out of that mess. It's conceptually possible to have compile time checking for this. Think of RefCell/Weak and .upgrade() and .borrow() being checked at compile time.
I've discussed this with some of the Rust devs. The trouble is traits. You'd need to know if a trait function could borrow one of its parameters, or something referenced by one of its parameters. This requires analysis that can't be done until after generics have been expanded. Or a lot more attributes on trait parameters. This is a lot of heavy machinery to solve a minor problem.
> Rust still needs a way out of that mess.
It has one: use raw pointers and unsafe. People are way too afraid of unsafe, it's there specifically to be used when needed.
> Rust still needs a way out of that mess.
In practice, it really doesn't. The difficulty of implementing doubly linked lists has not stopped people from productively writing millions of lines of Rust in the real world. Most programmers spend less than 0.1% of their time reimplementing linked data structures; rust is pretty useful for the other 99.9%.
Doubly linked lists are rare, but backlinks to the owner are often needed. It's the same problem, mostly.
Backlinks work fine with weak Arc references, don’t they?
Apologies since I have not taken the time to learn rust yet, but I've written a lot of modern C++. Is the ownership model kind of like std::unique_ptr and std::move, and `Rc<RefCell<Node>>` the same idea as `std::shared_ptr`? But less idiomatic? Or do I have the wrong idea?
Not really, because Rust enforces a "many readers or one writer" invariant on everything that has no C++ equivalent. That invariant is precisely what makes the doubly-linked list case hard (because every interior node in the list would be readable from two places, which means it can never be written to).
I am working on a code base, that among its many glories and poo balls every list is a doubly linked list.
Stop!
If you are using a doubly linked list you (probably) do not have to, or want to.
There is almost no case where you need to traverse a list in both directions (do you want a tree?)
A doubly linked list wastes memory with the back links that you do not need.
A singly linked list is trivial to reason about: There is this node and the rest. A doubly linked list more than doubles that cognitive load.
Think! Spend time carefully reasoning about the data structures you are using. You will not need that complicated, wasteful, doubly linked list
> There is almost no case where you need to traverse a list in both directions
But you might need to remove a given element that you have a pointer to in O(1), which a singly linked list will not do
If that's a specific use case you need to handle, it's O(1) again if you have a pointer to both the node to be removed and the previous node.
Whether it's more efficient to carry a second pointer around when manipulating the list, or store a second pointer in every list node (aka double linked list) is up to your problem space.
Or whether an O(n) removal is acceptable.
Getting the pointer to that element means randomly hopping around the heap to traverse the list though.
Linked lists are perfect for inserting/deleting nodes, as long as you never need to traverse the list or access any specific node.
So that you learn that loaning is for giving temporary shared^exclusive access within a statically-known scope, and not for storing data.
Trying to construct permanent data structures using non-owning references is a very common novice mistake in Rust. It's similar to how users coming from GC languages may expect pointers to local variables to stay valid forever, even after leaving the scope/function.
Just like in C you need to know when malloc is necessary, in Rust you need to know when self-contained/owning types are necessary.
The biggest thing I’ve run into where I really want self-referential types is for work that I want to perform once and then cache, while still needing access to the original data.
An example: parsing a cookie header to get cookie names and values.
In that case, I settled on storing indexes indicating the ranges of each key and value instead of string slices, but it’s obviously a bit more error prone and hard to read. Benchmarking showed this to be almost twice as fast as cloning the values out into owned strings, so it was worth it, given it is in a hot path.
I do wish it were easier though. I know there are ways around this with Pin, but it’s very confusing IMO, and still you have to work with pointers rather than just having a &str.
Note that some users of GC languages that support stack allocation, are used that it is a compiler error trying to have such a pointer/reference.
D example, https://godbolt.org/z/bbfbeb19a
> Error: returning `& my_value` escapes a reference to local variable `my_value`
C# example, https://godbolt.org/z/Y8MfYMMrT
> error CS8168: Cannot return local 'num' by reference because it is not a ref local
I'd rather pair program with someone wary of double-linked lists, but is really hot on understanding ownership than the other way around.
Because you care about productivity and safety more than l33t h4x0r hazing rituals?
Because most of my code is not doubly-linked lists!
It's not that it doesn't understand doubly linked list. It's just that you don't understand their consequences and have no objections against turning your program state briefly into inconsistent bullshit to facilitate them. The compiler minds. Unless you use Rc<>. That's what this language has for expressing inconsistency. Or unsafe {} if you are cocky. Borrows are not named pointers for a reason.
[dead]
Does anyone still have trouble learning Rust? I thought that was kind of a 2015 thing
It's very flattering! It's perhaps the only time in internet computing circles where I repeatedly see people argue in a way that boils down to:
'I'm not as good as learning things at you'
For me it was like Haskell. I spent an afternoon on it, my brain hurt too much, and I relegated it to the category of languages that are too complicated for what I need to do with a computer.
Languages I liked, I liked immediately. I didn’t need to climb a mountain first.
The thing is, once you internalized the concepts (ownership, borrowing, lifetimes), it's very hard to remember what made it difficult in the first place. It's "curse of knowledge" in some sense.
What's changed since 2015 is that we ironed out some of the wrinkles in the language (non-lexical lifetimes, async) but the fundamental mental model shift required to think in terms of ownership is still a hurdle that trips up newcomers.
100%. Newcomers still struggle a bit, especially if they've never used C/C++ before.
A good way to get people comfortable with the semantics of the language before the borrow checker is to encourage them to clone() strings and structs for a bit, even if the resulting code is not performant.
Once they dip their toes into threading and async, Arc<Lock<T>> is their friend, and interior mutability gives them some fun distractions while they absorb the more difficult concepts.
Do you mean `Arc<Mutex<T>>`? Yeah, I agree. Wrote a blog post on that topic as well: https://corrode.dev/blog/prototyping/ The title is a bit of a misnomer, but it's about beginner-friendly escape hatches in the language. Perhaps it's useful to newcomers.
Any lock, but that's generally the best choice.
Great post! It's got a ton of advice for being productive, and it should be especially useful for beginners.
Unfortunately, yes. I still end up writing C++ instead of Rust for low-level system stuff. Since I also know Go - I usually prefer that when I need lean, middleware services. Learned Rust (somewhat) with great difficulty but still don't use it anywhere. Still haven't figured out how to design effectively using Rust and approaches suggested in that article like clone()/unwrap() stuff and refactor later just leave a bad taste in that mouth.
Coming from C++, I don’t find it hard to learn. I do find it annoying, don’t love the syntax and absolutely hate cargo.
To each his own, I guess….
Yeah I struggled with it.