This document is intended for experienced developers coming from OO or FP backgrounds and are interested in learning Rust. It is my attempt at saving fellow travellers from slogging through the same bogs I did!
If you're coming from C/C++ you will probably learn Rust faster than someone coming from a memory managed runtime because you are used to thinking about where in memory data lives... Rust does not abstract that from you. It's somewhere between C and C++ in terms of complexity.
If you're coming from an OO languge you'll have to unlearn certain design habits. Rust is NOT an OO language, but does allow you to emulate some OO type patterns.
If you're coming from an FP language you'll likely be frustrated that Rust isn't "pure".
It values practicality and performance over FP purity, and attempts to address some of the
promises of FP in its own unique way (e.g. allows mutibuility, but in a highly constrained
way via its type-system). However, it's trait
system is a near cousin to Haskell
typeclasses, so you'll have a leg-up there.
-
Install rustup, either through brew or via curl script. Then run rustup update. More details available in the rustup book here.
-
Choose your editor. Each has their strengths and weaknesses. See Tooling section below.
-
If you have a license or work on non-commercial projects, RustRover is what I use because I've been using JetBrains products for decades. and am used to it.
-
VS Code with the
rust-analyzer
plugin. My collegues who don't use RustRover generally use VS Code. -
vim
withrust-analyzer
plugins. Not a vim user, but hear the integration is excellent.
-
-
cargo new my-first-rusted
Where to go depends on your style of learning., Popular options are:
More options in the Learning Resources section below.
-
Memory and thread safety above all else
-
You have to be explicit about what memory is used, everywhere.
-
Default is on the stack.
-
Functions are pass by value. You explicitly have to "borrow" (create a reference) to not copy, or to handle types that aren't
Copy
(i.e. don't implement theCopy
trait).
-
-
Zero cost abstractions
-
Makes no assumptions about compute resources. Can deploy to microcontrollers or compute clusters, with or without standard library or even a memory allocator.
-
Option
andResult
are often optimized to have no overhead (via "niche values"; depends on the contained type).
-
-
Only pay for what you need
-
Crates often have feature flags for selectively enabling/disabling capabilities.
-
derive
macros to inject functionality.
-
-
Single binary distribution
-
Astonishingly fast and lean on resource usage compared to managed runtime systems
-
Type system is very rich. Trait bounds are expressive.
-
Traits are closer to Haskell typeclasses than Scala traits.
-
Cargo is fantastic, especially compared to
sbt
ormaven
, but is not a generic build tool likecmake
. -
Native testing framework is no more complex than it needs to be.
-
Option
/Result
are fundamental, and everywhere. Often the compiler will optimize their overhead. -
The
serde
library is amazing out of the box. Just about every format. Serde will increase compile times. -
Macro system is hygienic and gives library providers a lot of power. Implementing your own macros is probably easier than in Scala, and has better error handling, but is still an advanced capability.
-
Cross compilation is amazingly seamless when no C libraries are involved.
-
Despite "pay for what you need", dependency hell isn't as present as expected. Exception is when C libraries like
openssl
are needed. -
Once you get used to them, macros are powerful, principled, and not to be feared. Often used to eliminate lots of boilerplate and configure auto trait generation (see
serde
). However, "proc" macros can increase compile times quite a bit. -
Compiles to WASM, and full-stack web app frameworks are coming onto the scene (but not yet mature). Note: Usually slower than JavaScript alone due to the communications bridge between browser interpreter and WASM sandbox.
-
Memory location is not abstracted away from you. You have to have a mental model of where data is going to be stored (stack, heap, thread local, shared, etc.), and the associated restrictions. The good part is that it's all through strong types.
-
You have to select your own preferred set of micro libraries, including async and concurrency libraries. Work in the community is ongoing to provide better abstraction points.
-
Orphan rule: you can implement traits for types you own, or for traits you define over types you don't own, but you can't implement foreign traits for foreign types.
-
Often multiple crates (libraries) for the same thing, of varying degrees of maturity. Like the early days of Scala where there's a lot of library competition and abandonware
-
It's not a pretty language. It can be verbose.
-
Lots of examples use
unwrap
onOption
/Result
. Bad practice! PropagateErr
orNone
with?
operator.anyhow
can help whenE
inResult<R, E>
doesn't type match, or you can usethiserror
's#[from]
attribute to unify with your other error types. -
String formatting is pretty verbose, but has improved.
-
No currying. :(
-
No method function overloading. (However, it can somewhat be simulated with generic traits.)
-
Larger frameworks are still evolving. For example, I think Axum is the most promising of the web service frameworks, but it's one of the newer ones, and behind Actix and Rocket in maturity. Kinda like when Sinatra and Play 2 were battling for supremacy, and Akka HTTP showed up.
-
The crates.io ecosystem has a flat namespace, so the create name alone (e.g.
s3
isn't enough to let you know if it's the best/most prominent/sanctioned implementation.
-
Memory types are a first-class concern in Rust. See Tips section below.
-
Embrace using macros, but judiciously. They are considered first class citizens in Rust. However, they can effectively inject a DSL into you code, affecting readability.
-
All traits on a type have to be requested/injected, often via
derive
macros. If you wantClone
orDebug
printing on a type, you have to ask for it. -
No runtime exceptions (but there is
panic
, which crashes the containing thread). AssumesResult
is used to propagate errors. -
Error handling is principled, but overly flexible. Use
anyhow
for error handling. Usethiserror
if creating your own error types. -
Sum/union types are via
enum
. Enums have three variants: unit-like, tuple, and struct. -
An
enum
variant is not a distinct type. There is no type projection for anenum
(e.g.MyEnum#Variant
) You cannot implement a trait for a specific enum variant. You cannot get inheritance-like behavior from Enums. This is frustrating. You have to implement your own match-based dispatch if you want that kind of behavior on an impl. -
No inheritance. Polymorphism is handled by generics (which are monomophized at compile time) or trait objects (fat pointers to the data struct and a
vtable
pointing to a trait implementation). -
No reflection on types. Everything is erased at compile time.
-
Self referential data structures require the use of reference counted pointers (
Rc
orArc
) or self-managed indexing. -
Orphan rule: you can only implement a trait when your crate either owns the trait, or owns the type for which the trait is implemented. If you own neither, you have to use the (verbose) "Newtype" pattern.
-
Extension methods are powerful but verbose. Use the
extend
crate. -
cargo build --release
can be significantly faster (and smaller) than the default debug build. -
There are three types of macros: declarative, procedural and derive. They have slightly different syntax and call site requirements.
-
Rust has "editions", rustc has versions; rustup has toolchains.
-
Just because a library hasn't been updated in a while doesn't mean it's abandoned... just that it's "done".
-
Lifetimes are only relevant when references are involved.
-
Until you become adept at lifetime parameters and borrow checking, don't hesitate to just
.clone()
when semantics allow. -
Sometimes you have to hold onto a reference on the stack to satisfy the borrow checker. This means that sometimes you have to introduce intermediate variables when you'd be inclined to chain method calls.
-
The
'static
lifetime doesn't mean that the value lived since the start of the program, but only that it lives to the end of the sub-program (will never be droped). -
Implementing
From
andTryFrom
for your types can significantly aid composability. -
Implement
Default
when possible, andnew
constructor method. For structs ofDefault
types, you can use#[derive(Default)]
-
Understand your memory (ordered by frequency of use, generally):
-
By default, Rust allocates everything on the stack. Rust won't let you access anything incorrectly, and you can actually do a lot with stack-alone allocations.
-
To allocate something on the heap, you use the
Box<T>
type. When the owning binding of aBox<T>
goes out of scope, the memory is deallocated. There is only a single owner of aBox<T>
(although you can borrow it, just like anything else). -
For shared ownership there is
Rc<T>
, which is a reference counted pointer, but for single-thread use. Rust is able to enforce this because it does not implementSend
, a requirement for moving data between threads. When a binding to anRc<T>
goes out of scope, the reference count is decremented. When it reaches zero, the object isDrop
ped. -
For shared ownership across threads there is
Arc<T>
, an atomic reference counted heap allocated object. It's able to be shared across threads because it does implementSend
. -
For "dynamic", safe mutability of shared state, there's
Cell<T>
andRefCell<T>
. For read-only data sharing but with the ability to change the value one sees locally, there isCow<T>
, a "copy on write" reference type.
-
-
Destructors in Rust are implemented with the
Drop
trait.
-
https://docs.microsoft.com/en-us/shows/beginners-series-to-rust/
-
https://blogs.harvard.edu/kapolos/rusty-ownership-and-the-lifecycles-stone/49/
-
https://www.chiark.greenend.org.uk/~ianmdlvl/rust-polyglot/index.html
-
https://rauljordan.com/rust-concepts-i-wish-i-learned-earlier/
-
https://users.rust-lang.org/t/how-to-think-without-field-inheritance/78116
-
https://rauljordan.com/rust-concepts-i-wish-i-learned-earlier/
-
https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html
-
https://dev.to/nicholaschiasson/beginner-s-guide-to-running-rust-on-aws-lambda-277n
-
https://aws.amazon.com/about-aws/whats-new/2021/12/aws-sdk-rust-developer-preview/
-
https://docs.aws.amazon.com/sdk-for-rust/latest/dg/getting-started.html
-
http://smallcultfollowing.com/babysteps/blog/2022/04/17/coherence-and-crate-level-where-clauses/
-
https://www.shuttle.rs/blog/2022/07/28/patterns-with-rust-types
-
https://tfpk.github.io/macrokata/ (Macros)
-
Cargo is amazing (despite using TOML)
-
RustRover Plugin is pretty good if you already use JetBrains products. It implements its own language server, which has some downsides.
-
VS Code gets the most attention. It uses the excellent
rust-analyzer
language server. Debugging is not as nice as Jetbrains, but errors and refactorings are more mature. -
Finding libraries: crates.io is the primary index (but it's a bit of a mess to navigate). libs.rs is a slightly more organized nice. IMO, a serious weakness is the lack of namespaces in the packaging system.
-
For minimalists: https://dystroy.org/bacon/
-
Crates hall-of-fame (in no particular order):
anyhow
clap
tokio
futures-util
rayon
chrono
/chrono-humanize
hyper
axum
mime
extend
log
envlogger
tracing
base64
serde
serde_json
serde_yaml
strum
num-traits
num-derive
syntect
crossterm
inquire
indicatif
load-dotenv
semver
ndarray
pyO3
kube-rs
See https://blessed.rs/crates for additional opinions.
-
This guy has some opinions on what a starter toolchain should look like. Of course, preferences vary.