About 1.5 years in Rust, experiences

After a year and a half of learning, it was interesting to write down what were the difficulties that just are gone, what remains hard, and what was easy.

Note: I still write Rust 90% in spare time, not at work, and have kids, so my time spans are longer.

Facepalms:

  • Half a year of experience I figured out that some CamelCase names in docs weren't types, but traits. I had read the book, saw a lecture on traits, but still they were in passive background knowledge.
  • A week ago, re-visiting some crates, I checked out their dependencies and saw "serde", which implied they were serde-compatible, while I had implemented this myself.
  • I would see some methods of structs in the docs, try using them -- and compiler said they weren't there. They came with traits. Took several times start paying attention and read above the method in the docs.

Hard things not noticed anymore:

  • Borrowing. The examples in the book are so primitive, they don't make you feel how it works.

      let x = vec![1, 2, 3];
      let y = x;
      println("{:?}", x);
    

    But it actually bites you later, when you realize that you can't borrow in a cycle. I ran into this about 8 months ago, and only then started to fully feel where things should be borrowed or not.

  • Crates features. It was very irritating when you write a method call, or import something, and it's not there.

  • It was impossible to tell where macros were imported from.

  • Macro/function distinction.

  • Type/trait distinction. It may be just my case, but it's easy to confuse them when you're beginner. Only after I banged my head against R-Star crate, I "got it".

  • Error handling mixed with iteration. You're irritated that it's that complicated, that you can't just implement "next". But then just get used to it.

  • Generics with trait bounds.

  • Iterations over vectors or hash(map|set) -- when I was almost a year into Rust, this was the biggest annoyment.

  • Piles of match clauses. At the beginning, match was the irritating duty to process all the options & results. Right now it's a safe fallback when methods (.map(), .ok...()) or let-else don't work.

Unexpectedly Easy Things

  1. Getting anything to work. After ~1 month from the very beginning, I took an algorithmically complex task that I earlier had written in Python, and re-wrote it, using just owned vars and clone() when needed. And it worked, a lot faster than Python.
  2. Parallelization and crossbeam. Was very straightforward, the syntax of closures is elegant too.
  3. Macros. It took several hours to read the small book on macros. But after you get it going, learning further in details is easy. A geat investment of effort that saves you a lot of code later.
  4. Testing. Initially you learn the structure like it were a boilerplate, and that's it -- you got it going.
  5. How to organize modules and import items. Turned out, you need 2-3 examples to get used to this.

Still Not Done

  • Async. Just purposefully avoided it for a while, to have 1 layer of complexity less.
  • Reading various file formats from different crates in one general-purpose API struct. Tried this 8 months ago and completely failed to marry owning and borrowing readers.

[ADDED on 10.9.23] Still Irritates

When a method is &mut self, and you call another method, blocked are the entire object and everyone that borrows it partially or entirely. You must out-refactor the call into methods of nested members or standalone functions:

impl<'a> MyRouter<'a> {
    pub fn method1(&'a mut self, ...) {
        self.method2(...) // THIS CALL causes entire structure to be borrowed
        // do something useful too on self
        let result = self.property1...
    }

    pub fn method2(&'a mut self, ...) {
        ...
    }
}

Feelings

The challenge of getting just any code to work was easy. It was very hard actually to get anything useful done -- like take a library and use it.

After 3-6 months, it was still hard to understand the terse docs of libraries, or make something compatible with other libraries (fit the trait boundaries which I didn't understand at all).

At 1 year, it was hard to iterate over structs or use a lib in a complex way.

At 1.5 years, biggest challenges are to architect and plan ahead.

Thanks to all who helped me here, you came at very critical, bottleneck moments.

20 Likes

Do you mean circular references, or are you implying that there's some aspect of how borrows interact with looping that's underexplained in the book?

Oh, I looked at the example and it's not the right one. The thing with borrowing is this:

let x = vec![1usize, 2, 3];
let y = &x;
some_func(y);

This works, but this won't:

let x: Vec<MyStruct> = vec![...];
let to_handle_later: Vec<&MyStruct> = vec![];
for item in x.iter() {
    if item.meets_criteria() { to_handle_later.push(item); }
}

(Or there should be another layer of own-borrow pair, IDK.)

Intuitively after Python, I thought I borrow from the value in the vec, but in reality the borrow is bound to var in the cycle, and is destroyed at the end of the iteration.

Why not?

#[derive(Debug)]
struct MyStruct {
    field: i32,
}
impl MyStruct {
    fn meets_criteria(&self) -> bool {
        self.field % 2 == 1
    }
}

fn main() {
    let x: Vec<MyStruct> = vec![
        MyStruct { field: 1 },
        MyStruct { field: 2 },
        MyStruct { field: 3 },
    ];
    let mut to_handle_later: Vec<&MyStruct> = vec![];
    for item in x.iter() {
        if item.meets_criteria() {
            to_handle_later.push(item);
        }
    }

    dbg!(&to_handle_later);
}
[src/main.rs:24] &to_handle_later = [
    MyStruct {
        field: 1,
    },
    MyStruct {
        field: 3,
    },
]

Quite the opposite… in Rust, this works fine, whereas in other “high-level” (and often claimed “easy/simple”) languages, writing similar looking code (incorrectly) might absolutely f#ing bite you. (I don’t use Go, so excuse any ignorance on why this behavior might be any reasonable. I was actually rather surprised when I first came across this, it seems like rather terrible programming language design, why doesn’t every iteration get its own local, immutable variable, instead of one persistent but mutable variable for the whole loop!? – this was only recently, because of recent release notes announcing the problem ought to be improved eventually, in the future.)


Of course in Rust, too, if the loop variable is by-value, any you create (and keep) references to it, then there’s a problem (as in, the code won’t compile):

#[derive(Debug)]
struct MyStruct {
    field: i32,
}
impl MyStruct {
    fn meets_criteria(&self) -> bool {
        self.field % 2 == 1
    }
}

fn main() {
    let x: Vec<MyStruct> = vec![
        MyStruct { field: 1 },
        MyStruct { field: 2 },
        MyStruct { field: 3 },
    ];
    let mut to_handle_later: Vec<&MyStruct> = vec![];
    for item in x.into_iter() { // <- now `into_iter` instead of `iter`
        if item.meets_criteria() {
            to_handle_later.push(&item); // <- now `&item`, not `item`
        }
    }

    dbg!(&to_handle_later);
}
error[E0597]: `item` does not live long enough
  --> src/main.rs:20:34
   |
18 |     for item in x.into_iter() {
   |         ---- binding `item` declared here
19 |         if item.meets_criteria() {
20 |             to_handle_later.push(&item);
   |                                  ^^^^^ borrowed value does not live long enough
21 |         }
22 |     }
   |     - `item` dropped here while still borrowed

For more information about this error, try `rustc --explain E0597`.

But fortunately, Rust will never introduce implicit shared mutability, so if there’s such a problem, it will always refuse to compile; whereas many other programming languages rather seem to follow an “implicit shared mutability absolutely everywhere” paradigm.

8 Likes

I expect this is due to obsolete example code using #[macro_use]. In modern Rust, when importing macros from a different crate, you can always write specific use imports instead.

2 Likes

True, but I've always found the macro scoping rules to be confusing. I thought #[macro_export] was especially weird, because it exports at the crate root instead of the module root. This still trips me up, and I still think creating macro_rules macros in a library is unwieldy.

2 Likes

I agree that the rules for exporting macros are not great. My point is that for importing macros, you can work only with sensible constructs and avoid the weird ones.

1 Like

Lots of old language have that, from the C89 history of only being able to declare variables at function-scope, or from a simple desugaring of for loops to while loops where the scoping for the condition is bad and thus it ends up defining the variables outside the loop body so it's available in the condition. (See also how do-while loops often can't have the condition look at variables declared inside the loop body, which would often be helpful.)

Newer languages know that was a bad idea and fix the scoping, but as always my big complaint with Go is that it's like it ignored everything that happened in the 15 years before it came out, and doesn't fix any of these kinds of things. (Yes, "lol no generics" was a meme, but for a good reason: Java and C# demonstrated clearly that, no, casting to object/interface{} in a statically typed language is not going to satisfy the users.)

If Go had came out in 1995, like Java, I probably would have really liked it. But it came out in 2009, when my expectations were much higher.

9 Likes

Good idea - however, I think this is also where the memory constraints imposed by the compiler pay off. Rust has many other features to recommend it - but I think async and multithreaded is where it really helps.

2 Likes

I'm curious, how did you do this/what do you mean? Did you use a workaround?

1 Like

No, this is new to me, thanks. :slight_smile:

In case of XML, I had made a struct that called QuickXML and assembled the target objects, like osmio, and another one to write to XML.

In case of geo::Point/LineString, I wrote a small mod that converted them to strings and applied it with #[serde(with="my_mod")].

I really like your post. It is very clear and reasonable. I have had all these problems too!

(And it is nice that you avoided critical comparisons to other languages that lead to never ending discussions.)

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.