I don’t think the book can be all things to all people, and really the only logical place for the book to start is at the beginning, which means assuming no programming experience. I think there is plenty of space for more targeted resources and when/if those mature they should be linked from the website in a similar way to the book.
I also felt the pain of learning Rust as an experienced C++ programmer, docs-wise. It’s still a pretty poor situation. I think that is an audience that will be interested and should be better served, but resources and priorities… I started writing Rust for C++ programmers to try and fill that gap, but it is pretty stalled due to lack of time. Contributions are most welcome!
On the subject of Cargo, I do think that it is somewhat unfortunate that we’ve ended up relying on it so much. I think there is something of an urge from many people, I believe heavily biased towards experienced systems programmers, to want to use ‘just the compiler’, at least initially. That might not be entirely rational, but I think we should cater to that need better than we do (r4cppp doesn’t really talk about Cargo, yet, although I suspect the instructions for building with rustc need updating).
I’ve often fantasised about a small set of checkboxes at the start of the book labeled like “I am familiar with C/C++” that magically customise parts of the tutorial based on your programming background. It’s probably way more trouble than it’s worth, though - it’s much simpler to just have a few notes throughout the book with titles like “note for higher-level programmers” that explain concepts such as the stack vs. the heap in a way that can be easily skipped by people who already understand their contents. Steve has been doing a great job at the essentially impossible task of catering for those without low-level experience while also avoiding boring everyone else, but I do wonder if his job could be made a bit easier if the book abandoned its one-size-fits-all mentality in some way or another.
Regarding the lifetime/ownership section of the book: everyone learns things differently, so there are always going to be people complaining about how it phrases things. Some changes proposed by those reading the guide will be definite improvements for everyone, but not all. It’ll never be perfect, and while we should strive for a good ownership guide, as always the best way to learn is through practice and experience.
I also had problems with the emphasis placed on Cargo, both in the Intro and early on in the book. While Cargo is important once you are going to be sharing code with other people, either by using libraries from crates.io or publishing your own libraries or applications, it’s fairly cumbersome for what a beginner is generally doing, which is writing a whole bunch of throwaway executables while playing around with things.
When I got started, I just wanted to be able to have a tmp/rust directory in which I would put my little test.rs, println.rs, b64.rs hacks, and easily edit them and compile them. Having them all in one flat directory with a Makefile that consists of:
%: %.rs
rustc $<
is a lot more convenient that repeatedly running cargo new println --bin and then having to navigate in to println/src/main.rs (which is the same filename for every project, making keeping buffers straight more frustrating) to edit each file. And figuring out how to get Cargo to handle a bunch of different binaries at the top level of a single project is a big detour via documentation on an entirely different site, which I didn’t want to get into to just write some quick throwaway code.
I think that the both the book and the intro should hold off on introducing Cargo until a little later, and when they do, it should be to introduce features that actually make it worthwhile, like adding dependencies on third-party libraries. But while just getting use to the syntax and semantics of the language, just using bare rustc (possibly wrapped in the user’s preferred existing build system) is a lot less cumbersome than trying to set up real Cargo projects for throwaway tests.
I think that perhaps one way to do this in the book would be to collect up some of these topics into a new secion, perhaps “Programming in the Large”, that comes between “Intermediate Rust” and “Advanced Topics”; that would include basic Cargo usage, depending on third party crates, tests, and documentation. That’s all stuff that everyone should learn, but I think that it can come after learning the basics of the language itself.
In the 30 Minute Intro, you could have just one small paragraph at the top to introduce rustc, and then put the Cargo section at the bottom, perhaps even with a motivating example for why you want to do this by actually adding a dependency on a third party library; there’s now enough good libraries on crates.io that there’s probably one that could act as a good motivating example (log or regex are a couple that seem like good choices).
I don't agree, here. While I think the book should cover the key concepts necessary for a programmer of any background to understand Rust, I do not think it should serve as a general introduction to programming. Trying to introduce Rust to existing programmers and teaching those with no experience to program in the same document would be a disservice to both.
The goal of the book should be to teach Rust, highlight its advantages, show how it's different, and and explain concepts needed to understand it and use it effectively. This will necessarily include information that some will already know, such as heap versus stack and memory addresses, but I think such information can be included as asides that can easily be read or skipped at the reader's discretion.
If someone wants to separately write an "Intro to Programming" type document that uses Rust, that's awesome, but I think it should be separate from the book.
Whoops, that sounds wrong on re-reading and I agree with you almost completely. The Rust book should not be teaching programming, it should be teaching Rust and it should assume the reader knows how to program. However, what I was getting at is that it shouldn’t assume the reader has any specific programming experience, i.e., it shouldn’t assume the reader is an experienced systems programmer, or knows C/C++ or Haskell or has experience with iterators or closures, etc.
As an aside, I think Rust is a terrible first programming language and we shouldn’t ever try and press that angle.
I'll echo this. Nearly every day I write some number of small Rust programs just to see what they do, and I always use rustc. I do think the operation of rustc itself is something people need to understand.
Maybe sections in the book that “can be skipped by C/C++ gurus” can be marked with a distinctive background colour, so they can be skipped more easily?
I also come from a C++ background and found myself agreeing with OP about the book. That said, the places that caught me out were:
I use DDG as a default search and DDG’s results for [rust are all out of date links like this][1]. That’s not in the rust community’s control, but I think if terms like ‘derive’ had links to the reference, then it would mean I don’t need to search. I’d be happy to do some of the grunt work of adding links if you point me to the book repo.
The part about ownership was not really clear.
Mixing concepts like generics + traits + ownership got me into the long grass quickly since there aren’t any examples in the book where they’re all used together (yet). That said, 5 mins on IRC and people helped me a lot. I think a small example where the different features are used would go a long way in showing people how it all fits together.
@nrc I don’t think Rust is a great first programming language, but I think it’s better than C++, which my university has been teaching as a first programming language for the last 15 years or so.
@steveklabnik here are the notes I’ve been taking while reading through the Intermediate sections. Looks like a big reorganization is in progress, so I better post this while it still has some relevance.
Pointers
This section explains why we should use &T in preference to Box<T>, before introducing Box<T>.
It doesn’t say why you can’t lend an mutable reference and another reference to the same value at the same time. (I remember hearing a rationale for this, involving enums, that seems irrelevant to why one can’t double-borrow simpler types like i32, but regardless of what the rationale is, it should be provided.)
More strings
We are told “References to Strings will automatically coerce into &strs”, but it doesn’t say why or how. It also doesn’t explain very clearly what the difference between str and String is. It says when to use which type, but since this is a systems language I’d like to hear an explanation in terms of memory layout.
It doesn’t say why there is a true in for l in s.graphemes(true).
Method Syntax:
it says one can “return self” to accomplish method chaining like foo.bar().baz(). This is true, but it refers to this as the “original example”, which is incorrect. The original example was x.foo().bar().baz() and it was given as an alternate syntax for baz(bar(foo(x))) in which no one would assume that each function returns self (x).
Associated types
What does the first line mean?
let graph = MyGraph; // MyGraph is the name of a struct
let obj = Box::new(graph) as Box<Graph>;
It makes exactly as much sense to me as let foo = i32; would. I also wonder why each of the structs do not have { bodies }.
Closures
“Anonymous functions that have an associated environment are called ‘closures’, because they close over an environment.” Although I know what a closure is, the phrase “close over an environment” isn’t meaningful to me. Must be a math thing.
I think rationales should be stated more explicitly. Instead of saying we don’t need to write the types of closure arguments because “they don’t cause the kinds of error-at-a-distance that inferring named function types can,” restate the fact that “Rust supports type inference, but doesn’t allow it on top-level functions because [blah blah blah]. For closures, however, these reasons don’t apply / these problems are not so severe / the benefits were felt to outweigh the drawbacks.” Although the “Basics - functions” section talked about this issue a little, it doesn’t hurt to repeat yourself, once, 15 sections later, especially since experienced developers may have skipped the Basics entirely.
The subsection on “move closures” is confusing because the previous example showed that variables are already moved into the closure without the move keyword, whereas the book says that the initial example with move gets a copy of num. The second example with move is also confusing, because the closure appears to get a copy of num, so I’m puzzled how the word move is relevant at all. Indeed, I am wondering if using move implies that all arguments must be copyable.
the closure traits are presented as if they were normal traits, but there are two strange things about them that are not explained:
The syntax Fn(A, B) -> C is different than for all other traits.
Moreover it seems impossible to express closures with the normal Trait<Params> syntax, because type Output is an associated type, not a type parameter, i.e. we can write Fn<(A, B)>, but there’s nowhere to put the return type C.
C++ programmers will recognize extern "rust-call" as a calling convention. But isn’t this redundant? Wouldn’t all trait functions be rust-call by default? I think if the book includes extern "rust-call" it needs to explain what it’s doing there.
Iterators:
Saying that for i in 0..nums.len() is “strictly worse than using an actual iterator” seems an exaggeration. Because if you use an index variable your loop will know which index of the vector it is looking at, while for num in &nums won’t. It’s a tradeoff.
The statement "Why does &nums give us references? Firstly, because we explicitly asked it to with &" is strange. There is no obvious connection between &nums and "&(nums[i]) for each possible i".
It is unclear why for num in &nums was later changed to for num in nums.iter().
Why doesn’t for i in (1..100).filter(|&x| x % 2 == 0) say *x % 2?
Generics:
A missed opportunity to give a rationale. The section ends by discussing "error: binary operation == cannot be applied to type T". C++ programmers will note that C++ does not give an error like that unless the template is instantiated with a T that actually has no == operator. The opportunity here is to explain that if a generic function compiles at all, then it will work correctly for all Ts that meet the constraints in the function signature. Hmm… this does mean that you’d have to have to introduce a constraint that includes == before the traits section. But that’s okay, there’s no need to know what a trait is before revealing T: Float.
Traits
PartialEq is a strange name. How is it “partial”?
Static & dynamic dispatch:
The VTable “example” is very confusing, firstly because it appears to be Rust code but on closer inspection seems to be a kind of pseudocode, secondly because it uses notations that were never introduced before, like x as *const u8, thirdly because call_method_on_u8 calls byte.method() somehow, even though call_method_on_u8 does not mention Foo nor FooVtable, and fourthly because there’s no apparent reason why the parameter x points to () instead of the type to which x is immediately cast. Finally, it seems to me that next to the pseudo-Rust we should be shown some normal Rust that the pseudo-Rust is supposed to represent. Since Rust is a systems language, isn’t it possible to write some code that is actually compiles & runs and is equivalent to what the normal Rust code does?
Macros:
I’m confused about this “vec” macro. It was shown with the syntax vec![1,2,3] but the square brackets are nowhere to be seen in the macro_rules.
I suggest changing the o_O macro to use the syntax 10 + [1, 2, 3] instead of 10; [1, 2, 3] so it’s more obvious what the macro is supposed to do.
Concurrency:
The problem with Mutex is fixed by introducing Arc, but you didn’t explain the fact that let data = data.lock().unwrap(); was orginally outside the closure and then was moved inside. I think the first version was inherently wrong, Arc or no Arc.
You mention that data.lock() may fail to acquire the mutex, but why would it fail? Doesn’t lock() wait for the shared resource to become available?
At the same location you do something that will baffle many programmers, let mut data = data.lock().unwrap();, in which the left and right hand side of = both refer to data. This trick wasn’t mentioned before, at least not in the Intermediate part of the book.
You seem to be suggesting that channels are somehow an alternative to using a timer to wait 50ms for threads to complete (even though you already introduced thread::scoped). I think it would be more appropriate to introduce join() at this point.
Error handling:
"try! makes use of FromError" links to a page that does not mention FromError.
A longer reply, to give you answers, in case you're still wondering:
(this is in the spirit of letting you know the answers to the questions, not saying that it's not a weakness of the docs.
In general, this is meant to assume that you have knowledge of Rust already. But most of this is going away in the re-organization, so it should be less confusing anyway.
It's just that this causes a data race.
It's true, this is 'deref coercions'. There's a whole section in the new TOC abou tit.
[quote="qwertie, post:52, topic:1816"] It also doesn't explain very clearly what the difference between str and String is.
[/quote]
That's a bummer, as it's a major part of this section
str is a primitive type that's not sized. String is a library type, that's an owned, heap-allocated string.
Empty structs don't need bodies. I basically just left
struct MyGraph;
out of the example, maybe it'd be more clear with it there. I was trying to say that it works for any struct, rather than unit structs only.
I think recent drafts have extra text here, unsure.
That is the blah blah blah. Errors at a distance.
Yeah, this is because numbers are Copy, which are copied on move. Maybe shouldn't use a Copy type.
yeah, this is basically sugar.
The associated type is named Output.
Yeah, I left this in for accuracy, but didn't want to get into calling conventions.
You can use enumerate() if you also need an index, so this case isn't actually an advantage.
I'll review the copy-editing in this section. They're equivlanet forms.
Because it's not a pointer: &x pattern matches a pointer, and x is a value.
Wait, in C++, if a template compiles, it still may not work? that sounds... terrible. This is just me not being familliar with C++ details, and assuming things work as they should
Partial equality as opposed to total equality.
it is.
[quote="qwertie, post:52, topic:1816"] secondly because it uses notations that were never introduced before, like x as *const u8,
[/quote]
that's a cast, which has its own section in the new TOC
Right, the intention here is to show the underlying structure which the compiler uses.
[quote="qwertie, post:52, topic:1816"] and fourthly because there's no apparent reason why the parameter x points to () instead of the type to which x is immediately cast. Finally, it seems to me that next to the pseudo-Rust we should be shown some normal Rust that the pseudo-Rust is supposed to represent. Since Rust is a systems language, isn't it possible to write some code that is actually compiles & runs and is equivalent to what the normal Rust code does?
[/quote]
The problem here is the clash with the builtin types....
all macros can be invoked with (), {}, or [].
I guess that closures capturing things they referred to was known.
of the top of my head, no idea.
[quote="qwertie, post:52, topic:1816"]Doesn't lock() wait for the shared resource to become available?
At the same location you do something that will baffle many programmers, let mut data = data.lock().unwrap();, in which the left and right hand side of = both refer to data
[/quote]
This is called 'shadowing' and is pretty pervasive in Rust code, I'll make sure it's in the bindings section.
Not at all, it's just to show a program that does something.
Yeah, this type got removed RIGHT BEFORE beta, so links break I'll update it.
This was an attempt to address the cryptic differences between PartialOrd and Ord which is probably similar to the differences between PartialEq and Eq (the bug is probably a little out of date though now). The fact that I attempted to give a simpler example 3-4 times and had to be corrected each every time should be an indicator that it's kinda complicated and should be simplified/clarified.
If you have any concrete suggestion on how this could be explained better, do state your ideas. It would be good if these concepts were explained better.
To clarify: it's dangerous even in the single-threaded case. Consider some C++ like:
void foo(std::vector<std::string> xs)
{
for (const auto &x : xs) {
// ...
if (some_condition) xs.push("whoops"); // causes UB
}
}
This kind of error is impressively common in real C++ codebases, and is particularly insidious because it might only be apparent under unusual circumstances (like if the vector's capacity needs to grow + relocate during the iteration). Ensuring that a pointer through which mutation can occur has unique access to its referent, as with Rust's &mut T, prevents this class of errors entirely. Anecdotally, I've noticed an example like this is far more likely to catch the attention of a seasoned C++ programmer than others.
Yeah, it's a good example and I've seen some C++ programmers reacting very negatively on it! I've seen Rust described as "a language where you can't insert/remove elements in the loop" several times.
C++ gives much more fine grained guarantees for such things, than just "UB happens" (see here for example) and people don't like to trade this freedom and control for safety.
If the new size() is greater than capacity() then all iterators and references (including the past-the-end iterator) are invalidated. Otherwise only the past-the-end iterator is invalidated.
vector::push_back probably isn't the best example, since you can only keep using any pointers or iterators if you actually checked capacity, which isn't a common pattern from what I've seen. Of course, there are common situations where C++ programmers can and do keep relying on pointers, like removing elements from a vector, modifying elements in place, modifying lists, etc.
I think the best answer to such complaints is to say that you don't have to feel bad about using vector indices everywhere if necessary - yea, even if it reminds you of heathen languages that don't have pointers.
This, I know that everything wants you to install it first before you play with it, but it would help so much if the tutorial was just clicking play embedded in the page and getting the result instantly back.