Over the last few months I’ve been teaching myself Rust. It’s a language designed with rock-solid memory safety in mind compared to languages like C++. It features an ownership system and the infamous ‘borrow-checker’ to eliminate many memory safety bugs at compile time.
I wanted to experiment with it after using Perspective for a project at work. It’s an open-source UI component built for visualising large amounts of streaming data. It was a surprise looking through the code to find it was written in Rust!
I’ve learnt a lot by using the borrow-checker, but my favourite features of the language have actually been completely unrelated to memory management:
- Support for generics in enums. You can define enums that you would need a fully-fledged class for in C#. An example is the below, which is a very simple implementation of union types:
enum Either<L, R> {
Left(L),
Right(R),
}
- The
Resulttype.Resultis a special generic enum that represents the outcome of some operation. It can either beOk(T), representing success with a return value of type T, orErr(E)representing an error of type E. This is a very powerful construct that is used in a lot of internal libraries, for things like I/O and exception handling. For example, to open a file in Rust and handle any errors you can do:
let file = File::open("file.txt").unwrap_or_else(|err| println!("Failed to open file: {}", err));
File::open returns a Result<File>. unwrap_or_else then either returns the successful value out of the Result, or calls the given lambda if the Result was an error. This is considerably cleaner than the C# equivalent, which involves more lines of code and an extra level of indentation.
try
{
var stream = File.OpenRead("file.txt");
}
catch (Exception e)
{
Console.WriteLine($"Failed to open file: {ex.Message}");
}
- Perhaps my favourite feature: all variables are immutable by default unless marked with
mut. This makes debugging so much easier compared to usingvar/letin C# or Typescript, where you have to watch out for variables being modified as you step through. This is also enforced at compile time.
➜ 10_references git:(main) ✗ cargo run
Compiling references v0.1.0 (/Users/fawaz/learning/rust/rust_book/10_references)
error[E0596]: cannot borrow `s` as mutable, as it is not declared as mutable
--> src/main.rs:3:5
|
3 | s.push_str("Modified!");
| ^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut s = String::from("Hello, world!");
| +++
For more information about this error, try `rustc --explain E0596`.
- Finally, extremely helpful error messages! The Rust compiler will point to the exact line and expression with a mistake. Here, when I reference an undeclared variable, it suggests one with a similar name:
➜ git:(main) ✗ cargo run
Compiling references v0.1.0 (/Users/fawaz/learning/rust/rust_book/10_references)
error[E0425]: cannot find value `mystring` in this scope
--> src/main.rs:7:31
|
7 | println!("The length of '{mystring}' is {length}");
| ^^^^^^^^ help: a local variable with a similar name exists: `my_string`
For more information about this error, try `rustc --explain E0425`.
And here’s what it prints when I make the mistake of borrowing a value that has been moved into a function. It highlights the three contributors of the error!
➜ git:(main) ✗ cargo run
Compiling ownership v0.1.0 (/Users/fawaz/learning/rust/rust_book/9_ownership)
error[E0382]: borrow of moved value: `s`
--> src/main.rs:6:16
|
2 | let s = String::from("hello"); // s comes into scope
| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 |
4 | takes_ownership(s); // s's value moves into the function and so is no longer valid here
| - value moved here
5 |
6 | println!("{s}"); // this would cause a compile-time error!
| ^ value borrowed here after move
|
For more information about this error, try `rustc --explain E0382`.
You can see after every error above, there’s a helpful rustc --explain command that gives more details about the error, with examples.
It’s worth mentioning that aside from the typical compilation targets like x86_64, Rust can also be compiled to WebAssembly, or wasm. This is a low-level instruction format designed to be executable by all major web browsers. Since 2017 it has been supported on Chrome, Firefox, Safari, Opera, and more.
It’s how web components like Perspective can be written in Rust, completely outside the web ecosystem! You can see some examples of data visualised with Perspective here.