My first month of Rust

  1. Pros
  2. Struggles
    1. Types
    2. Ownership
    3. Other
  3. Conclusion

Originally published at the Telia Engineering Blog

A month ago I have started learning Rust and would like to share my impressions, the good things I have appreciated, and the things I have struggled with. Why Rust, do you ask? Primarily to challenge myself, to leave the land of managed runtimes (Clojure, JavaScript) and to get as close to the metal as you can without assembly. Knowing a systems (&more) programming language is handy, for example for writing fast serverless functions and command-line utilities. Why not Go? For the same reasons why Clojure: it is more innovative, more mind-bending. Go is optimized, I understand, for approachability (and performance, of course) and is popular for writing web services - but it failed to capture the C/C developers at Google it was aimed at, I hear. Rust's focus is on performance and safety, the latter forcing it to take a really innovative approach to the issue of memory management. And some experienced C/C developers swear by it. So Rust already seemed more attractive to me and reading Bryan Cantrill’s Falling in love with Rust and Sylvain Wallez' Go: the Good, the Bad and the Ugly sealed the deal. From the former:

Rust feels like a distillation of the best work that came before it.

Platforms reflect their values, and I daresay the propagation operator is an embodiment of Rust’s: balancing elegance and expressiveness with robustness and performance.

So, what have been my experiences so far? I have spent the past 30 days reading Rust books and going through the Rust exercises at Exercism.io, benefiting immensely from the help of my mentors, especially Lewis Clement (thank you so much!).

Pros

Let me start with the positives:

  • Great error messages - clear, helpful, try to suggest a solution (one of the best features I have experienced!)

  • Great support from IntelliJ - especially method completion, display of documentation, and display of the inferred types were very helpful for me as a beginner. Navigation to the definition of a symbol is also helpful.

  • Tooling - built in, simple to use package manager, test framework and runner, formatter. The compiler was reasonably quick (for my tiny code (though slower to run tests than in Clojure REPL :)))

  • Good support for functional programming with lambdas and a powerful Iterator API (e.g. .rev() for going in reverse, etc.)

  • Familiar, C-ish syntax

  • Operators (at least some?) are only a sugar for method calls and can be extended to your own types by implementing the corresponding traits. I find this cool. As a Clojurist I am used to having only functions and no operators :).

  • Good and clear support for safe multithreading programming with channels, mutexes, etc.

  • Obviously great integration with calling to/from C

  • An expression-based language - for example if returns the value of the executed branch (so no need for the JS ternary operator ?:!); something I am already used to from Clojure

  • Very good documentation (standard documentation with examples, multiple great on-line books such as Rust by Example - which I started with - The Rust Programming Language, and the Rust Cookbook)

  • Smart type inference so that I do not need to type the types :) all the time in function bodies

  • A few widely used and applicable conversion functions such as Iterator::collect and into

  • Support for code examples in the docs that are actually run and checked upon compilation ♥️

  • Error handling is reportedly very good though I have too little experience to make my own opinion

  • Immutable by default (though it isn’t Clojure’s immutability)

(Just for the record, I still strongly prefer Clojure and its interactive development and simplicity, though obviously the two have (mostly :)) completely different use cases.)

Struggles

Types

Perhaps my greatest struggle was satisfying the type checker and understanding why my types did not match. Some examples:

  • I wanted to split a Vec into chunks but there is no iterator that does that (in std; there is a library for it); however there is the .chunks() iterator for slices. Can/should I turn the Vec into a slice to be able to use it (and perhaps back afterwards)? What would be the cost of that?

  • Complex return types. From Clojure I am used to every sequence function simply returning a sequence while in Rust the return types represent the operation and stack up: my_vec.iter().rev()Rev<Iter>>. (That’s the price for this zero-cost abstraction.) See the type of the combine_vecs_explicit_return_type function before it was simplified to impl Iterator for another example.

  • core::iter::once(1) and once(1).chain(once(2)) have incompatible types though logically both are Iterator<u32>; same with empty() and once()

  • From the slice &[&str] I got Iterator<String> but needed to return HashSet<&str> and struggled to convert it somehow. (I had to give up on .map(|w| w.to_lowercase()) because that was what turned the &str into a String)

    iter vs. into_iter - what is the difference and how does it matter? What is the difference between Iter and IntoIterator (and Iterator)?! Looking at the docs for Vec I found this:

impl<'a, T> IntoIterator for &'a Vec<T>
  fn into_iter(self) -> Iter<'a, T>
  //Creates an iterator from a value.

impl<T> IntoIterator for Vec<T>
  fn into_iter(self) -> IntoIter<T>
  //Creates a consuming iterator, that is, one that moves each value out of
  //the vector (from start to end). The vector cannot be used after calling this.

so based on whether I have a reference or a value (if I’m reading it correctly), I get a different result. It is hard to understand what this actually means / consequences without broader and deeper knowledge of Rust.

Ownership

I understand ownership, moving, copying and borrowing in theory but do not grasp it and its implications fully in practice. I struggle to grasp when do I need x vs. &x vs. *x; and it gets even more complicated when I use some of the iterator API and end up e.g. with &&&x (fortunately I have been pointed to Iterator::cloned() to get rid of one level of & though I would hardly find it myself; example of the problem: assert_eq!(1, ["abc"].iter().filter(|s| **s == "abc").count());). This PostgreSQL example confuses me because it uses conn.execute but &conn.query (perhaps because the latter is used in for .. in and we don’t want the for loop to take ownership of conn and destroy it?). As a beginner I do not know which types implement core::marker::Copy and thus are copied vs. moved. When do I want/need to use .to_owned()?

I also struggled to understand when I need to dereference and when not because some methods were happy with a reference while other methods/operators required a value. From C I am used to that whenever I have a pointer I must use the dereferencing instead of . while in Rust it seemed to be somewhat random. (Presumably because a method is just a function that takes as the first argument self - or &self. So it is clear from the types - if you know and understand them.) It helped somehow when I stopped thinking in the terms of values vs references and starting to thing of owning vs borrowing.

Other

Though there is a lot of great documentation online and it is awesome that the language docs include examples, one grievance is that the language docs are hard to navigate for me - when I search for Copy I get 5 matches in Dash: one "S" (?), two traits (core::marker::Copy and its re-export in std) and two macros (std::marker::Copy, core::Copy). In this case it was the two traits that actually had the docs I have been looking for. Later I discovered that the "S" result was the best as it pointed to Rust reference section on Copy. When I search for "Copy" in the std crate, I get ±200 matches. I am sure that I will eventually learn to search and interpret results better but I am not there yet.

Understanding and distinguishing between builtin data structures, especially Vec, arrays, and slices - they are all similar but different. (I understand now that Vec is on the heap and can grow while an array is fixed-size.)

Syntax - remembering what means what (especially macro syntax much more complicated than Clojure’s for obvious reasons) (Clojure’s syntax is trivial, the devil is in the semantics :))

Confusion by having the same functions and types in std and core (until I understood that std just re-exports the core types for convenience).

Conclusion

I am learning a lot and it is great to do something completely different. I hope I get to write things in Rust so that I get deeper experience especially with the parts foreign to me. I wish Clojure tried to have so helpful error messages (though limited by the compiler’s ignorance of the "types").


Tags: rust languages


Copyright © 2024 Jakub Holý
Powered by Cryogen
Theme by KingMob