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.
> 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.
> Could owner be something else than a stack frame?
Yes. There are lots of ways an object might be owned:
- a local variable on the stack
- a field of a struct or a tuple (which might itself be owned on the stack, or nested in yet another struct, or one of the other options below)
- a heap-allocating container, most commonly basic data structures like Vec or HashMap, but also including things like Box (std::unique_ptr in C++), Arc (std::shared_ptr), and channels
- a static variable -- note that in Rust these are always const-initialized and never destroyed
I'm sure there are others I'm not thinking of.
> 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
Here are some example situations where you'd "pass by value" in Rust:
- You might be dealing with "Copy" types like integers and bools, where (just like in C or C++ or Go) values are easier to work with in a lot of common cases.
- You might be inserting something into a container that will own it. Maybe the callee gets a reference to that longer-lived container in one of its other arguments, or maybe the callee is a method on a struct type that includes a container.
- You might pass ownership to another thread. For example, the main() loop in my program could listen on a socket, and for each of the connections it gets, it might spawn a worker thread to own the connection and handle it. (Using async and "tasks" is pretty much the same from an ownership perspective.)
- You might be dealing with a type that uses ownership to represent something besides just memory. For example, owning a MutexGuard gives you the ability to unlock the Mutex by dropping the guard. Passing a MutexGuard by value tells the callee "I have taken this lock, but now you're responsible for releasing it." Sometimes people also use non-Copy enums to represent fancy state machines that you have to pass around by value, to guarantee whatever property they care about about the state transitions.
> Why would a stack frame want to move ownership to its callee
Happens all the time in modern programming:
callee(foo_string + "abc")
Argument expression foo_string + "abc" constructs a new string. That is not captured in any variable here; it is passed to the caller. Only the caller knows about this.
This situation can expose bugs in a run-time's GC system. If callee is something written in a low level language that is resposible for indicating "nailed" objects to the garbage collector, and it forgets to nail the argument object, GC can prematurely collect it because nothing else in the image knows about that object: only the callee. The bug won't surface in situations like callee(foo_string) where the caller still has a reference to foo_string (at least if that variable is live: has a next use).
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.
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.
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.
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 usually teach it by translating it to our physical world by way of an object like a book, which I like to think is intuitive.
I have a book. I own it. I can read it, and write into the margin. Tear the pages off if I want. I can destroy it when I am done with it. It is mine.
I can lend this book in read only to you and many others at the same time. No modifications possible. Nobody can write to it, not even me. But we can all read it. And borrower can lend it recursively in read only to anybody else.
Or I can lend this book exclusively to you in read/write. Nobody but you can write on it. Nobody can read it; not even me; while you borrow it. You could shred the pages, but you cannot destroy the book. You can share it exclusively in read/write to anybody else recursively. When they are done, when you are done, it is back in my hands.
I can give you this book. In this case it is yours to do as you please and you can destroy it.
If you think low level enough, even the shared reference analogy describes what happens in a computer. Nothing is truly parallel when accessing a shared resource. We need to take turns reading the pages. The hardware does this quickly by means of cached copies. And if you don't want people tearing off pages, give then a read only book except for the margins.
> the shared reference analogy describes what happens in a computer. Nothing is truly parallel when accessing a shared resource. We need to take turns reading the pages. The hardware does this quickly by means of cached copies.
I still haven't gotten into rust yet, mostly due to time and demand, but, I have been doing a lot of C++ in the past few years.
Coming from that background these rules sound fantastic, theres been a lot of work put into c++ the past few years to try and make these things easier to enforce but it's still difficult to do right even with smart pointers.
The main problem is that a lot of things that are correct wrt lifetimes will still not compile because the borrow checker can't prove that they are correct. Even for fairly trivial stuff sometimes, like trees with backlinks.
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).
In my experience, understanding the rules of the borrow checker is not enough to be able to write rust code in practice. For example, ~6 months into using rust I was stumped trying to move data out of a mutable reference. Trying to do this directly by dereferencing gives compiler errors like "cannot move out of `*the_ref` which is behind a mutable reference". If you know rust, you're probably either yelling "you idiot! you can't move out of mutable references!" or "you idiot! just use std::mem::take!" (the latter of course being the right way to do this) but that's not obvious from the borrow checker rules.
My experience learning rust has been like a death by 1000 cuts, where there's so many small, simple problems that you just have to run into in the wild in order to understand. There's no simple set of rules that can prepare you for all of these situations.
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.
Rust feels like an excellent language paired with a beta-quality borrow checker. The issue is that the more they fix the paper cuts, the more complex the type system grows.
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.
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.
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.
For the most part true, but there exists patterns I can do safely and easily in c++ which I cannot in rust. Structured concurrency being one of the major ones. If a child object takes a reference to the parent, I shouldn't need to do it by way of an Arc, but because of the fact that leaking memory is safe, this isn't possible to do in rust without using the unsafe keyword. So I end up with more refcounting that I want. (This is often in the context of async). I don't bring this pattern back to c++ with me.
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.
- 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.
Python is growing type annotations at a brisk pace though, and Typescript is cannibalizing Javascript at an incredible speed. Between that and even Java getting ADTs, I suspect the people who whine about "type nerds" are in for some rough years as dynamic languages lose popularity.
And I suspect the people who are familiar with seeing something like `dict[str, int]` can map that onto something like `HashMap<String, i32>` without actually straining their brains, and grow from there.
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.
"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.
The importance of a concept is not related to its frequency of direct use by humans. The https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_equation is one of the most important concepts in options trading, but AFAIK quants don't exactly sit around all day plugging values into it.
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).
I think this is good insight, and I would extend this further to “coming from a less strict language to a very strict one”.
As someone who self-learned Rust around 1.0, after half a year of high school level Java 6, I’ve never had the problems people (even now) report with concepts like the ownership system. And that despite Rust 1.0 being far more restrictive than modern Rust, and learning with a supposedly harder to understand version of “The Book”.
I think it’s because I, and other early Rust learners I’ve talked to about this, had little preconceived notions of how a programming language should work. Thus the restrictions imposed by Rust were just as “arbitrary” as any other PL, and there was no perceived “better” way of accomplishing something.
Generally the more popular languages like JS or Python allow you to mold the patterns you want to use sufficiently, so that they fit into it.
At least to me with languages like Rust or Haskell, if you try to do this with too different concepts, the code gets pretty ugly. This can give the impression the PL “does not do what you need” and “imposes restrictions”.
I also think that this goes the other way, and might just be a sort of developed taste.
For whatever it's worth, I used to do a lot of teaching, and I have a similar hunch to
> I think it’s because I, and other early Rust learners I’ve talked to about this, had little preconceived notions of how a programming language should work. Thus the restrictions imposed by Rust were just as “arbitrary” as any other PL, and there was no perceived “better” way of accomplishing something.
As a low level programmer, my biggest pain with rust was the type system. More precisely the traits and the trait bounds.
I suspect if you have C++ experience it's simpler to grokk, but most of the stuff I wrote was C and a bunch of the stuff Rust did were not familiar to me.
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.
Works that way with learning a spoken language, too. I couldn't learn my second language until I stopped thinking I was supposed to judge whether things in the language were "good" or not. Languages aren't meant to be "good" in a beauty contest sense, they're supposed to be useful. Accept that they are useful because many, many people use them, and just learn them.
I probably wouldn't have been able to do that with Rust if I hadn't been an Erlang person previously. Rust seems like Erlang minus the high-overhead Erlangy bits plus extreme type signatures and conscious memory-handling. Erlang where only "zero-cost abstractions" were provided by the language and the compiler always runs Dialyzer.
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.
There's no discussing taste, especially with syntax. Personally I find the Rust syntax unoffensive, while the Go syntax comes off kind of … weird, with type signatures especially looking kind of like run-on sentences by someone who hates punctuation. C's type signatures come off as a turgid mess to me; stuff like this https://www.ericgiguere.com/articles/reading-c-declarations.... is just a design mistake as far as I'm concerned. And Python … kind of goes into the "ah, I give up, slap an `Any` signature on it" territory.
And some people love that! It just ain't for everyone.
I love the look and feel of Nim, but found it to be stuck in a weird chicken-and-egg situation where it didn't have enough of a following to have a Convenient Package For Everything, ultimately turning me off it. Of course I recognize that the only way a language gets a Convenient Package For Everything is if it gets popular, but still...
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.
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.
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.
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...
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.
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.
> _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.
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:
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.
Yes. There are lots of ways an object might be owned:
- a local variable on the stack
- a field of a struct or a tuple (which might itself be owned on the stack, or nested in yet another struct, or one of the other options below)
- a heap-allocating container, most commonly basic data structures like Vec or HashMap, but also including things like Box (std::unique_ptr in C++), Arc (std::shared_ptr), and channels
- a static variable -- note that in Rust these are always const-initialized and never destroyed
I'm sure there are others I'm not thinking of.
> 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
Here are some example situations where you'd "pass by value" in Rust:
- You might be dealing with "Copy" types like integers and bools, where (just like in C or C++ or Go) values are easier to work with in a lot of common cases.
- You might be inserting something into a container that will own it. Maybe the callee gets a reference to that longer-lived container in one of its other arguments, or maybe the callee is a method on a struct type that includes a container.
- You might pass ownership to another thread. For example, the main() loop in my program could listen on a socket, and for each of the connections it gets, it might spawn a worker thread to own the connection and handle it. (Using async and "tasks" is pretty much the same from an ownership perspective.)
- You might be dealing with a type that uses ownership to represent something besides just memory. For example, owning a MutexGuard gives you the ability to unlock the Mutex by dropping the guard. Passing a MutexGuard by value tells the callee "I have taken this lock, but now you're responsible for releasing it." Sometimes people also use non-Copy enums to represent fancy state machines that you have to pass around by value, to guarantee whatever property they care about about the state transitions.
Happens all the time in modern programming:
callee(foo_string + "abc")
Argument expression foo_string + "abc" constructs a new string. That is not captured in any variable here; it is passed to the caller. Only the caller knows about this.
This situation can expose bugs in a run-time's GC system. If callee is something written in a low level language that is resposible for indicating "nailed" objects to the garbage collector, and it forgets to nail the argument object, GC can prematurely collect it because nothing else in the image knows about that object: only the callee. The bug won't surface in situations like callee(foo_string) where the caller still has a reference to foo_string (at least if that variable is live: has a next use).
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.
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.
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.
All the jargon definitely distracted me from grasping that simple core concept.
But if you come from Javascript or Python or Go, where all this is automated, it's very strange.
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.
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.
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?
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.
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 have a book. I own it. I can read it, and write into the margin. Tear the pages off if I want. I can destroy it when I am done with it. It is mine.
I can lend this book in read only to you and many others at the same time. No modifications possible. Nobody can write to it, not even me. But we can all read it. And borrower can lend it recursively in read only to anybody else.
Or I can lend this book exclusively to you in read/write. Nobody but you can write on it. Nobody can read it; not even me; while you borrow it. You could shred the pages, but you cannot destroy the book. You can share it exclusively in read/write to anybody else recursively. When they are done, when you are done, it is back in my hands.
I can give you this book. In this case it is yours to do as you please and you can destroy it.
If you think low level enough, even the shared reference analogy describes what happens in a computer. Nothing is truly parallel when accessing a shared resource. We need to take turns reading the pages. The hardware does this quickly by means of cached copies. And if you don't want people tearing off pages, give then a read only book except for the margins.
This exists, but it's uncommon: https://en.wikipedia.org/wiki/Dual-ported_RAM , https://en.wikipedia.org/wiki/Dual-ported_video_RAM
And what is that? Its easy to fall in the trap of making explanations that is very good (if you already understand).
Coming from that background these rules sound fantastic, theres been a lot of work put into c++ the past few years to try and make these things easier to enforce but it's still difficult to do right even with smart pointers.
My experience learning rust has been like a death by 1000 cuts, where there's so many small, simple problems that you just have to run into in the wild in order to understand. There's no simple set of rules that can prepare you for all of these situations.
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.
With the exception of the last point (which I can't imagine tacking on in C++)
Maybe I'm missing some subtle point?
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.
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.
- 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 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.
And I suspect the people who are familiar with seeing something like `dict[str, int]` can map that onto something like `HashMap<String, i32>` without actually straining their brains, and grow from there.
This is what's stumped me when learning Rust. It could be the resources I used, whixh introduced macros early on with no explanation.
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.
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.
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.
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).
As someone who self-learned Rust around 1.0, after half a year of high school level Java 6, I’ve never had the problems people (even now) report with concepts like the ownership system. And that despite Rust 1.0 being far more restrictive than modern Rust, and learning with a supposedly harder to understand version of “The Book”.
I think it’s because I, and other early Rust learners I’ve talked to about this, had little preconceived notions of how a programming language should work. Thus the restrictions imposed by Rust were just as “arbitrary” as any other PL, and there was no perceived “better” way of accomplishing something.
Generally the more popular languages like JS or Python allow you to mold the patterns you want to use sufficiently, so that they fit into it. At least to me with languages like Rust or Haskell, if you try to do this with too different concepts, the code gets pretty ugly. This can give the impression the PL “does not do what you need” and “imposes restrictions”.
I also think that this goes the other way, and might just be a sort of developed taste.
> I think it’s because I, and other early Rust learners I’ve talked to about this, had little preconceived notions of how a programming language should work. Thus the restrictions imposed by Rust were just as “arbitrary” as any other PL, and there was no perceived “better” way of accomplishing something.
I suspect if you have C++ experience it's simpler to grokk, but most of the stuff I wrote was C and a bunch of the stuff Rust did were not familiar to me.
> 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
I probably wouldn't have been able to do that with Rust if I hadn't been an Erlang person previously. Rust seems like Erlang minus the high-overhead Erlangy bits plus extreme type signatures and conscious memory-handling. Erlang where only "zero-cost abstractions" were provided by the language and the compiler always runs Dialyzer.
Python is my favourite, C is elegance in simplicity and Go is tolerable.
C has a simple syntax, but it is most certainly not elegant.
It may be impractical for some tasks but the power:complexity rate is very impressive. Lua feels similar in that regard.
And some people love that! It just ain't for everyone.
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.
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.
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.
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.
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.
Bondage driven development.
https://www.youtube.com/@jonhoo