@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
&Tin preference toBox<T>, before introducingBox<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
strandStringis. 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
trueinfor 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 wasx.foo().bar().baz()and it was given as an alternate syntax forbaz(bar(foo(x)))in which no one would assume that each function returnsself(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 thestructs 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
movekeyword, whereas the book says that the initial example withmovegets a copy ofnum. The second example withmoveis also confusing, because the closure appears to get a copy ofnum, so I’m puzzled how the wordmoveis relevant at all. Indeed, I am wondering if usingmoveimplies 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) -> Cis different than for all other traits. - Moreover it seems impossible to express closures with the normal
Trait<Params>syntax, becausetype Outputis an associated type, not a type parameter, i.e. we can writeFn<(A, B)>, but there’s nowhere to put the return typeC.
- The syntax
- C++ programmers will recognize
extern "rust-call"as a calling convention. But isn’t this redundant? Wouldn’t all trait functions berust-callby default? I think if the book includesextern "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, whilefor num in &numswon’t. It’s a tradeoff. - The statement "Why does
&numsgive us references? Firstly, because we explicitly asked it to with&" is strange. There is no obvious connection between&numsand "&(nums[i])for each possiblei". - It is unclear why
for num in &numswas later changed tofor 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 typeT". 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 revealingT: Float.
Traits
-
PartialEqis 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 becausecall_method_on_u8callsbyte.method()somehow, even thoughcall_method_on_u8does not mentionFoonorFooVtable, and fourthly because there’s no apparent reason why the parameterxpoints to()instead of the type to whichxis 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 themacro_rules. - I suggest changing the
o_Omacro to use the syntax10 + [1, 2, 3]instead of10; [1, 2, 3]so it’s more obvious what the macro is supposed to do.
Concurrency:
- The problem with
Mutexis fixed by introducingArc, but you didn’t explain the fact thatlet data = data.lock().unwrap();was orginally outside the closure and then was moved inside. I think the first version was inherently wrong,Arcor noArc. - You mention that
data.lock()may fail to acquire the mutex, but why would it fail? Doesn’tlock()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 todata. 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 introducejoin()at this point.
Error handling:
- "try! makes use of
FromError" links to a page that does not mentionFromError.
Thanks for listening!