Newcomer to Rust: my experience

@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:
    1. The syntax Fn(A, B) -> C is different than for all other traits.
    2. 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.

Thanks for listening!

2 Likes