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!).
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
ifreturns 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
- 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.)
Perhaps my greatest struggle was satisfying the type checker and understanding why my types did not match. Some examples:
- I wanted to split a
Vecinto 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:
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 Iteratorfor another example.
once(1).chain(once(2))have incompatible types though logically both are
Iterator<u32>; same with
- From the slice
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
into_iter - what is the difference and how does it matter? What is the difference between
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.
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; 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.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
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.
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 (
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
core (until I understood that
std just re-exports the
core types for convenience).
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").