Hacker News new | past | comments | ask | show | jobs | submit login

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.

[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:

    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.

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.

I think it's funny how I had this kind of sort of "clear" understanding of Rust ownership from experience, and asking "why" repeatedly puts a few holes in the illusion of my understanding being clear. It's mostly familiarity of concepts from working with C++ and RAII and solving some ownership issues. It's kind of like when people ask you for the definition of a word, and you know what it means, but you also can't quite explain it.

I would say you're correct that ownership is something that only exists on the language level. Going back to the documentation: https://doc.rust-lang.org/book/ch04-01-what-is-ownership.htm...

The first part that gives a hint is this

>Rust uses a third approach: memory is managed through a system of ownership with a set of rules that the compiler checks.

This clearly means ownership is a concept in the Rust language. Defined by a set of rules checked by the compiler.

Later:

>First, let’s take a look at the ownership rules. Keep these rules in mind as we work through the examples that illustrate them:

>

>*Each value in Rust has an owner*.

>There can only be one owner at a time.

>*When the owner goes out of scope*, the value will be dropped.

So the owner can go out of scope and that leads to the value being dropped. At the same time each value has an owner.

So from this we gather. An owner can go out of scope, so an owner would be something that lives within a scope. A variable declaration perhaps? Further on in the text this seems to be confirmed. A variable can be an owner.

>Rust takes a different path: the memory is automatically returned once the variable that owns it goes out of scope.

Ok, so variables can own values. And borrowed variables (references) are owned by the variables they borrow from, this much seems clear. We can recurse all the way down. What about up? Who owns the variables? I'm guessing the program or the scope, which in turn is owned by the program.

So I think variables own values directly, references are owned by the variables they borrow from. All variables are owned by the program and live as long as they're in scope (again something that only exists at program level).


> Ownership exists at the language level, not the machine level.

Right. That's the key here. "Move semantics" can let you move something from the stack to the heap, or the heap to the stack, provided that a lot of fussy rules are enforced. It's quite common to do this. You might create a struct on the stack, then push it onto a vector, to be appended at the end. Works fine. The data had to be copied, and the language took care of that. It also took care of preventing you from doing that if the struct isn't safely move copyable.

C++ now has "move semantics", but for legacy reasons, enforcement is not strict enough to prevent moves which should not be allowed.


> 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]);

> // Put something after v on the heap

> // so it can't be grown in-place

> let v2 = v.clone();

I doubt rust guarantees that “Put something after v on the heap” behavior.

The whole idea of a heap is that you give up control over where allocations happen in exchange for an easy way to allocate, free and reuse memory.


It certainly doesn't guarantee it, this is just what's needed to induce a relocation in this particular instance. But this makes Rust's ownership tracking even more important, because it would be trivial for this to "accidentally work" in something like C++, only for it to explode as soon as any future change either perturbs the heap or pushes enough items to the vec that a relocation is suddenly triggered.

That’s correct.

The analogous program in pretty much any modern language under the sun has no problem with this, in spite of multiple references being casually allowed.

To have a safe reference to the cell of a vector, we need a "locative" object for that, which keeps track of v, and the offset 0 into v.


> The analogous program in pretty much any modern language under the sun has no problem with this, in spite of multiple references being casually allowed.

And then every time the underlying data moves, the program's runtime either needs to do a dynamic lookup of all pointers to that data and then iterate over all of them to point to the new location, or otherwise you need to introduce yet another layer of indirection (or even worse, you could use linked lists). Many languages exist in domains where they don't mind paying such a runtime cost, but Rust is trying to be as fast as possible while being as memory-safe as possible.

In other words, pick your poison:

1. Allow mutable data, but do not support direct interior references.

2. Allow interior references, but do not allow mutable data.

3. Allow mutable data, but only allow indirect/dynamically adjusted references.

4. Allow both mutable data and direct interior references, force the author to manually enforce memory-safety.

5. Allow both mutable data and direct interior references, use static analysis to ensure safety by only allowing references to be held when mutation cannot invalidate them.


That’s a different implementation, and one you can do in Rust too.

> 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).


> _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.

Is this still true if they never learned pre-modern C++ and are accustomed to using all the std::foo_ptrs and expecting the rule of 3 (or 5) to be taken care of automatically that way?

The prereq for my Rust course is Java, but there are three kinds of students who come to me:

1) those who only know java

2) those who know java and were taught C++ by me. The way I teach that course, they are very familiar with pre-modern C++ because we also learn C.

3) those who know java and C++ but they learned it on their own.

It's the last group who has the most trouble. IME the exact issue they struggle with is the idea of shared mutable state. They are accustomed to handing out pointers to mutable state like candy, and they don't worry about race conditions, or all the different kinds of memory errors that can occur and lead to vulnerabilities. They don't write code that is easily refactored, or modular. They have a tendency to put everything into a header or one main.cpp file because they can't really get their head around the linking errors they get.

So when they try to write code this way in Rust, the very first thing they encounter is a borrow error related to their liberal sharing of state, and they can't understand why they can't just write code the way they want because it had been working so well for them before (in their very limited experience).

Pedagogically what I have to do is unteach them all these habits and then rebuild their knowledge from the ground up.


> So when they try to write code this way in Rust, the very first thing they encounter is a borrow error related to their liberal sharing of state, and they can't understand why they can't just write code the way they want because it had been working so well for them before (in their very limited experience).

Ah, well, a shame they didn't see the failing tests for the C++ code first ;)


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.


Ya, I just think the `mut` thing isn't as much of a challenge to newcomers as the memory management aspect.

"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.

If you're in hard realtime land then you can't have any allocations at all; that's a pretty different ballgame.

Destructors should be as simple and side-effect free as possible, with the exception of things like locks or file handles where the expectation is very clear that the object going out of scope will trigger a release action.


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.

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.


It's more than that. Rust's value & reference passing semantics are completely different from the way most programmer's have trained their entire lives to think about it.

When you pass an argument to a function in Rust, or assign a value into a struct or variable, etc. you are moving it (unless it's Copy). That's extremely different from any other programming language people are used to, where things are broadly pass by value pass by reference and you can just do that as much as you want and the compiler doesn't care. It's as if in C++ you were doing std::move for every single argument or assignment.

And so as a programmer you have to shift to a mindset where you're thinking about that happening. This is profoundly unintuitive at first but becomes habit over time.

Then having that habit, it's actually a nice reasoning skill when you go back to working in other languages.


I think sometimes that there's a niche for an alternative syntax for Rust that is generally more verbose (keywords instead of punctuation etc), and which specifically makes this behavior explicit. In other words, for every argument of every call you'd have to write "move" or "copy" and similarly with assignments etc.

RustRover recently added visual indication of the borrowing process within a block of code, which is kind of nice. I think for now it only shows for some cases and only when there's an error? but still you get a highlight of where the borrow happened so you can walk back to it and figure out how to fix it without having the parse through the compile error.

I suspect they'll eventually get a fast and live indicator to the user of where all the references are "going" as they type.


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).

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.


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 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.

Rust has succeeded in making us hesitant reaching out to raw pointers. To the point we sometimes forget that we can opt-in into unsafe raw pointers on demand. Unlike C/C++ being opt-in to maybe somewhat safe.

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:

    - the object is destroyed
    - the program core dumps
    - it is a compile time error
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).


I think the Brown University’s modifications to the rust book do an excellent job of explaining the borrow checker.

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 only way I could understand the borrow-checker was to implement my own version. Then it made sense.

I like how you phrase it, but it's missing the mutable XOR shared for references.

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.

What do you mean? What are some examples of borrow checker improvements that resulted in more type system complexity?

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.


> 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.

"Flattening the learning curve" is perhaps a wrong metaphor - you can't actually change what needs to be learned; you can only make it easier to learn.

Saying something that is usually right and can be corrected later is a standard pedagogical approach - see https://en.wikipedia.org/wiki/Wittgenstein%27s_ladder . To extend the metaphor, the ladder is there to help you climb the learning curve.


It's not "usually right" though. Rust can't compile a doubly-linked list[1] without unsafe!

And people trip over this immediately when they start writing Rust, because that kind of code is pervasive in other environments. Thus statements like "Rust just doesn't like dangling pointers" are unhelpful, because while it's true it's not sufficient to write anything but the most trivial code.

[1] Or basically any graph-like data structure that can't be trivially proven to be acyclic; even lots of DAG-like graphs that "should" be checkable aren't.


People write non-trivial code all the time without worrying about that sort of thing. Quite a lot can be done with plain tree structures. In the real world, your data is flat and even your conventions for interpreting it as non-flat (such as, say, JSON) only create trees that perhaps simulate back-links with another informal protocol.

Sigh. The whole premise of the linked article is, in fact, that people hit validation problems with the borrow checker early on when learning rust and that attention is needed to "flatten the learning curve" to assist their understanding of what we all agree is a unique and somewhat confusing set of semantics relative to competing languages.

Rust flaming is just so terribly exhausting. No matter how reasonable and obvious a point is there's always someone willing to go to the mattresses in a fourty-comment digression about how Rust is infallible.


... Would it help you to know that I don't even use Rust (although I'm interested in picking it up), and in fact have complained on HN before about program performance and other features being invalidly attributed to "it's written in Rust"? Especially in the Python ecosystem, that being my primary programming language?

I'm not making anything like the argument you seem to think I am. I'm only making a pragmatic observation about what real-world coding is like, based on my own experience.


You should try picking up Rust, and then, based on your Python experience, see how quickly you run into one of those cases that GP is talking about. It's unlikely to take long.

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.

> But for some reason, people always reverse this meaning, and so the common idiom breaks down for people who try to reason it out.

Because one imagines the "curve" like physical topology, with the goal of reaching the top.


> ... 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.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: