Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

first draft of 'ownership' #58

Merged
merged 14 commits into from Jan 19, 2016
Merged

first draft of 'ownership' #58

merged 14 commits into from Jan 19, 2016

Conversation

steveklabnik
Copy link
Member

r? @aturon

Rendered: https://github.com/rust-lang/book/blob/ownership/src/ownership.md

I should add the diagrams before we merge this. EDIT: diagrams added


Before we get to the details, two important notes about the ownership system.
Rust’s central feature is called ‘ownership’. It is a feature that is
straightforward to explain, but has deep impleications for the rest of the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: impleications -> implications

@nikomatsakis
Copy link

Read it and thought it was excellent. One question I had, which I already mentioned to @steveklabnik on IRC, was whether it makes sense to introduce moves first as shallow copies and then lead up to the errors that would result if you didn't have moves (but oh we do!), or whether it's better to first describe it as a move, and then show why a move makes sense. I'm not really sure.

@aturon
Copy link
Member

aturon commented Jan 13, 2016

I agree with @nikomatsakis that this is really good work! I have some detailed notes below -- the biggest wording issue is around confusion with the term "copying" (not always clear when you mean deep and when shallow).

From a high-level perspective, I think you're hitting almost all the right notes here, with one exception: you're not emphasizing enough that Rust is statically checking ownership for you. This should be more of a running theme, and I think it's worth noting that this is a major innovation in Rust that allows it to provide memory safety without garbage collection, amongst numerous other advantages that we'll see later in the book.

Ownership

Rust has a focus on safety and speed. It accomplishes these goals
through many ‘zero-cost abstractions’, which means that in Rust,
abstractions cost as little as possible in order to make them work.

Instead:

Rust is committed to both safety and speed. One of the key tools for
balancing between them is "zero-cost abstractions": the various
abstractions in Rust do not pose a global performance penalty.

This often happens because the programmer’s mental model of how
ownership should work doesn’t match the actual rules that Rust
implements.

Instead:

This can happen because the programmer isn't used to thinking
carefully about ownership, or is thinking about it differently from
the way that Rust does.

Variable binding scope

This variable binding refers to a string.

Did you mean: string literal?

Memory and allocation

In languages without a garbage collector, they often force you to
call a second function to give the memory back. Part of the
difficulty of languages that work like this is knowing exactly when
to do so.

Instead:

Languages without a garbage collector often force you to call a second
function to give the memory back. Part of the difficulty of using such
languages is knowing exactly when to do so.

I like the point about RAII. We should use the book to standardize on
a better name for Rust. We've talked about "Ownership-based resource
management" as one possibility in the past.

When we assign s1 to s2, the String itself is copied

When a value moves, its data is copied,

This needs to say shallowly copied and be very emphatic about it,
because it's a key point for this example.

Also, you first mention shallow copying, then take a digression on the
String representation, but you never come back explicitly to say the
key thing: Shallow copying means copying the pointers, but not the
data being pointed to. You should contrast this with deep copying,
where there are now two independent copies of the actual contents,
each of which can be mutated independently.

Definitely worth dwelling on these points.

Clone

But what if we do want to copy the String’s data?

Say: deeply copy. For this section, important to be crystal clear each
time you talk about copying.

Copy

In other words, a call to clone() would do nothing special over
copying the data directly.

A better way to say this is:

In other words, there's no difference between deep and shallow copying
here, so calling clone() wouldn't do anything differently from the
usual shallow copying.

any simple value that only represents some memory can be Copy

How about:

any group of simple scalar values can be Copy, but nothing that
requires allocation or is some form of resource is copy.

Ownership and functions

This is good, and quite clear. But I think you should also take this opportunity to re-emphasize the static checking point. I would suggest that, with the example with takes_ownership(s);, you just mention again that if you try to use s thereafter you get a compiler error.

@steveklabnik
Copy link
Member Author

I pushed up a commit which takes care of all of this feedback, except maybe the extra emphasis on compile-time checks. Gonna think more carefully about when to do this. I don't want to do it too much either.


The double colon (`::`) is a kind of scope operator, allowing us to namespace this
particular `from()` function under the `String` type itself, rather than using
some sort of name like `string_from()`.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This explanation feels very out of place. Perhaps we should cover that before getting here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We haven't yet talked about anything related to this, and forcing all of the materials about modules to be this early in the book would be a bummer. I do dislike forward references though...

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if String::from is self explanatory enough to hand wave away?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly appreciated this note @steveklabnik, having not worked in Rust for a while but having passing familiarity it felt like it was a worthy aside to me. It gave just enough I was no longer curious and ended up googling another doc, and it isn't overly-distracting.

Addendum: I come from Ruby-land originally, so :: in another language always catches my eye. So there is definitely a bias to my thought process

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this be simplified to something similar to:

The double colon (::) indicates we are using a function from a specific module. In this case we use the from() method located inside the String module. That is the only thing you need to know for know, we will talk about modules in length a little bit later.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@azerupi That actually makes it a bit more clear than the current text, I like it a lot.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a bit of misinformation, as it's not getting it out of the String module at all, but calling a static method on the String type (as the current text notes). Maybe it's a small enough lie, though.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appealing to constructors may be useful for those who are familiar with the notion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stood out to me too. Too much kinda/sorta. Maybe "Calls from function of the String type"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a bit of misinformation, as it's not getting it out of the String module at all, but calling a static method on the String type

Indeed, I realized my mistake after posting it. We should avoid misinformation even for the sake of simplification. It could end up exploding back into our face.

The double colon (::) indicates we are using a function that is located inside a module or associated with a type. In this case we use the from() method associated to the String type. That is the only thing you need to know for know, we will talk about modules and functions associated to types (traits forward reference?) in length a little bit later.

Is "functions associated to / with types" semantically correct? I wanted to find something visually simple to understand instead of bringing up the traits jargon.

@sgrif
Copy link

sgrif commented Jan 13, 2016

Some various thoughts about this:

  • The examples as written basically assume an understanding of pointers. Would box be a more straightforward type to use as an example than String? The term String doesn't imply heap allocation to those new to the language, while it's Box's entire reason for existence.
  • The tone of this, especially in the beginning, seems to be "Hey here's this thing, it's scary, but you can work around it and here's how". I feel like it's missing an example demonstrating why it's awesome and you should be excited about it. Certainly an RAII example would be good, but it might be cool to showcase how certain types of bugs are impossible (e.g. Rails changed @env[PATH_PARAMETERS_KEY] ||= {} to @env[PATH_PARAMETERS_KEY] || {} and it broke RSpec, but that bug would have been impossible with the borrow checker)
  • I very much prefer your examples involving function calling to those involving variable assignment. They feel more explicit, and the fact that assigning a variable can error, while necessary, makes it feel more naggy than helpful.

@steveklabnik
Copy link
Member Author

Thanks @sgrif!

I chose String over Box here for a few reasons:

  1. Strings are a really common pain point for new Rust programmers.
  2. Boxes aren't used very often in idiomatic Rust
  3. I want to lead into slices soon (it goes ownership-> references -> slices), and so using a collection over a single heap-allocated value is useful. It also lets us actually explain string literals.

I feel like it's missing an example demonstrating why it's awesome and you should be excited about it.

I think this ties into @aturon's comments above about compile-time checking. Need to motivate this better, for sure.

I very much prefer your examples involving function calling to those involving variable assignment.

Ah, interesting. Hmmmmmmmm..... I like this idea.

@sgrif
Copy link

sgrif commented Jan 13, 2016

FWIW tying it into function calling could also help out with the explanation of String::from

[Variable bindings][bindings] have a property in Rust: they ‘have ownership’
of what they’re bound to. This means that when a binding goes out of scope,
Rust will free the bound resources. For example:
For this, Rust has a second string type, `String`. You can create a `String` from

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make it clear that the first type is &str here, instead of relying on the user to have recognised that from the error message - they might see it, assume the &str is scary and unrelated and not have that immediately cleared up for them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this is a good point. I'm being a bit evasive here because we haven't seen the & symbol at all, and it's explained in the very next section. But that it appears in an error message might be a good reason for a forward reference.

@steveklabnik
Copy link
Member Author

https://twitter.com/maskoficarus/status/687422424225296385 <- maybe I should list some standard types which are Copy.

@steveklabnik
Copy link
Member Author

The error around + is a little odd: https://twitter.com/rseymour/status/687425389568856064

@steveklabnik
Copy link
Member Author

As means of motivation, it might be worth talking about how this is not reference counting, and why. (This might be good for one of those "this is advanced, don't worry about it" sections)

@brianarn
Copy link

Hello!

Based on a quick interaction with you on Twitter, @steveklabnik, I read this chapter. Here's a couple of thoughts and my responses to questions posed there, coming at this as someone with a couple of decades of programming experience, but zero Rust exposure.

Before getting to that though: This chapter makes me want to read the next one. :) It's really well-written and makes Rust intriguing. If I weren't such a heavy front-end person right now, I'd be all like "NEED TO TRY THIS RIGHT NOW".

Thank you for your work here! Quality documentation is the BEST! 🎉 🎊

Thoughts on Ownership

If one can describes a part of the learning curve as "fighting with" the language semantics, and it states that people will continue to fight with it over time, it makes the language sound fairly unappealing and difficult, and already makes me feel nervous about trying anything with the language. I like the bit of encouragement at the end of that paragraph, though. :) This feeling didn't persist, but feels worth noting.

The second example changes from let s = "hello"; to let mut s = "hello"; - is mut something covered in a prior chapter? In the context, it sounds like a way to mark a variable as mutable vs. constant, but I could see that throwing a newbie. If it's already covered, ignore me here. :)

The idea of Move vs Clone is interesting, but feels rather foreign and distinct within Rust. I feel like I understand the reasons behind it, now, but I didn't after my first pass. I think it clarified when writing my answers below.

To answer the two questions posed on Twitter:

Does this make any sense to me?

Yes, it makes sense to me. :)

The technical details are quite clear. The code examples are simple, concise, and the detailed versions with comments are great. I also like the memory diagrams.

I understand that when I assign a variable to another variable, if it is not marked as Copy or a primitive, it's akin to doing a shallow copy where pointer values and primitives are copied over, but the original variable has its contents "moved", and is thus invalid, throwing a compile-time error. Likewise, when I pass something into a function, it is also moved, assuming it is not marked as Copy or a primitive.

Do I have a sense of why this feature is valuable?

Originally, I wrote "No, I do not" but the truth is that I think I do understand the reason, but I don't feel like I really understand the full value right now, with just this chapter.

There is a line that says "Remember: If we tried to use s after the call to takes_ownership(), Rust would throw a compile-time error! These static checks protect us from mistakes." I have no idea what these mistakes would be, based on this reading. Even a simple example, or a demonstration of a mistake that Ownership saves me from, would be worthwhile here. If it can't be done at a newbie level, it might not work to put that here, but that sentence feels very hand-wavy to me, which may be fine if this is an early chapter and you feel like it gets covered better elsewhere.

I can understand that Ownership is the answer to the balance between safety and speed -- I think. It keeps me safe in the sense that I don't have to think about things like malloc etc, but it preserves speed because there's no GC / reference counting etc. That being said, I didn't have a good sense of this reason until writing this answer out.

Explicitly stating the above reason (assuming I got the right reason) would be of some value in this chapter, I think. It's there in a sense, in the section discussing being at a crossroads, and I pulled it out after a bit more review, but it feels like it'd be a nice summary at the end of a section. That way, it could more clearly tie the decisions behind Ownership back to the safety / speed balance as introduced at the beginning of the chapter.

Final Thoughts

I do feel like I've read it and it all makes sense, but I feel like some of the clarity I got was because I was trying to answer the above questions. Without those guiding questions, I'm not sure I'd have really seen this value or understood the decision behind Ownership.

I really enjoyed reading it, and as this solidifies, I really want to read the book in full now. :)

Thank you again for your efforts here! It's awesome. :)

@iwinux
Copy link

iwinux commented Jan 14, 2016

This is an excellent chapter!

As what I replied on Twitter, mentioning "shallow copy" really help people with Python / Ruby background understand what is going on. For C++ guys, the "move" semantics may sound more straightforward, though.

There's one more thing that seems unclear to me:

let s1 = String::from("hello");
let s2 = s1;

Here s1 is moved into s2 - does it involve any real operation of moving memory around?

@athaeryn
Copy link

This is great! I'm just getting in to Rust, and documentation like this is making it really fun! 🎉

One thing that I found a little confusing was near the end of the section on Copy: you talk about types "being Copy." Is it common in the Rust community to refer to types that implement traits as being that trait?

Also, nits: I just noticed two places in the last paragraph of that section where you don't have Copy capitalized and/or in backticks.

Thanks for the work you're putting in to this; I look forward to reading the next chapters! :)

checker’, where the Rust compiler refuses to compile a program that the author
thinks is valid. This can happen because the programmer isn’t used to thinking
carefully about ownership, or is thinking about it differently from the way
that Rust does. You probably will experience similar things at first. There is
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"experience something similar at first"?

There’s a problem here! Both `data` pointers are pointing to the same place.
Why is this a problem? Well, when `s2` goes out of scope, it will free the
memory that `data` points to. And then `s1` goes out of scope, and it will
_also_ try to free the memory that `data` points to! That’s bad.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's bad long before s1 goes out of scope! Actually it's bad before either do!

@Gankra
Copy link

Gankra commented Jan 14, 2016

Big Picture

  • This is section is ultimately about affinity, which is only one aspect of ownership. I like to think of ownership as affinity+borrows/regions+privacy. I'm concerned you could come out of this thinking ownership == affinity.
  • I'm concerned that this reinforces the idea that ownership is basically just about memory management. You touch on it a bit with TcpConnection that this isn't necessarily the case (my goto is File fwiw).

Will the subsequent sections be reinforcing that ownership is actually a larger thing, and useful for more than memory management? (iterator/view invalidation, use-after-close, data races, sequencing!)

Medium Picture

  • Not clear how much you do or don't want to use certain jargon (allocate, free, leak, reference).
  • Copy is super overloaded as a word throughout (consider using more: deep, shallow, memcopy, bitwise-copy, move, clone)
  • Not clear how low-level you want to get

@StyMaar
Copy link

StyMaar commented Jan 14, 2016

There is good news, however: more experienced Rust developers report that once they work with the rules of the ownership system for a period of time, they fight the borrow checker less and less. Keep at it!

You don't just fight it less, you deeply thank him for pointing your mistakes ! :)

String literals are convenient, but they aren’t the only way that you use strings. For one thing, they’re immutable. This will not work:
let mut s = "hello";
s = s + ", world!";

Immutability is not the problem here. This works fine.

let mut s = "hello";
s = ", world!";

The problem is that &str doesn't implement the “Add” trait.

Mixing the concepts of mutability with where the values are allocated could be quite misleading for beginners imho.

@steveklabnik
Copy link
Member Author

@athaeryn Thanks!

One thing that I found a little confusing was near the end of the section on Copy: you talk about types "being Copy." Is it common in the Rust community to refer to types that implement traits as being that trait?

Yes, it's the way it's normally used.

@steveklabnik
Copy link
Member Author

@brianarn thanks! If you check out the other PRs, you can in fact read the next two sections.... they're just even less polished than this one is 😉

@steveklabnik
Copy link
Member Author

Thank you everyone for your feedback! There's still some tweaking I want to do with this, in particular, highlighting why this matters more, but I'm going to do that in another PR. Thanks so much!

steveklabnik added a commit that referenced this pull request Jan 19, 2016
@steveklabnik steveklabnik merged commit 1de1dd7 into master Jan 19, 2016
@steveklabnik steveklabnik deleted the ownership branch January 19, 2016 16:10
amitu pushed a commit to FifthTry/rust-book that referenced this pull request Jun 1, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet