About Rust
Rust is a low level language with a twist. It has a modern set of features that high level languages usually have, while it’s performance is more similar to C or C++.
From the Rust website:
Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.
According to the Stack Overflow Developer Survey, Rust is the most loved language by 73% of developers. Also, check out those benchmarks - Rust Vs Java. Sounds good right?
I have seen a lot of articles talking about Rust, most of them mentions Rust’s memory management and concurrency features from a high level point of view. From what I gathered, if you want to get started with writing Rust you should read “The Book”, which covers most of the language’s features and how to use them. The book suggests that after reading it, you would be comfortable with writing Rust programs. Well.. I can’t exactly agree. I finished reading “The Book” recently and I have to say that the learning curve feels quite steep. However, some of Rust’s features should be very intuitive for more experienced developers.
Unlike “The Book”, after reading this you shouldn’t be comfortable with coding in Rust. Instead of talking about Rust’s features from a high level view, I’ll show you some of the features and try and give you an idea of how it looks and feels, as well as what makes it different.
The Parts You’d Expect
I’ll go over them quickly with some code examples, so we can get to the more interesting parts.
Newer developers, with only one or two programming languages in their toolbox, might not understand some parts of this article. So I apologize in advance.
Variables, References, Stack, Heap
Defining variables is done with the let keyword.
let x = 10; // define the variable x with a value of 10
let my_string = "a string!" // strings are a bit more complex in rust but this is a simple example
let mut y = 20 // define a mutable variable y with a value of 20
(Note that Rust’s variables are immutable by default - we’ll get to that later)
As you can probably guess, the compiler implicitly assigns types for you when possible.
Rust has all the basic types - int, float, bool, char, string, array, tuple, etc. Nothing out of the ordinary.
There’s a stack and a heap, like in most programming languages. They have more importance here, but again nothing new.
In the following example some_string will be allocated on the heap and will be passed by reference:
let some_string = String::from("bla bla");
some_function(&some_string); // pass a reference of some_string to some_function
There are different Rust rules at play here, but don’t worry about them at the moment.
Functions, Loops, If Else
The function syntax is as following:
fn another_func(i: u32) -> u32 { // This function accepts one parameter
// of type u32 (unsigned int) and also
// returns one u32
// function body
}
Rust has a for loop, a while loop and a loop loop. it also has iterators:
loop {
// someone break me or i will run forever
}
while x < 5 {
x = x -1;
}
for item in some.iter() {
println!(item);
}
The if else is pretty common, but you can return a value from it like a function:
let var1 = if y == 1 {
// do whatever
"equals 1" // leaving out the ; would return this value into var1
} else {
// do something else
"not equals 1"
}
Structs Are Kind Of Like Objects But Not Really
If you like Go, Rust’s structs should make you feel at home. If you are an Object Oriented person, you would probably call this a class.
The Car struct:
struct Car {
make: String,
model: String,
wheels: u32
}
You can also write methods:
impl Car {
fn drive(&self) {
println!("vroom");
}
}
Make an instance of the Car struct in something like a constructor:
impl Car {
fn new() -> Car {
Car {
make: String::from("Ronda"),
model: String::from("Fonda"),
wheels: 6,
}
}
}
In Rust, the new method is called an “associated function”. You might know this concept as “static method”. There’s no default constructor, or anything like that.
Public, Private, Modules
Rust has modules - you need to follow some rules on how to structure your files, but basically it’s something like this:
mod my_module {
fn my_function() {
}
}
You’re modules and functions will default to being private, but you can make them public like this:
pub mod my_module {
pub fn my_function() {
}
}
Vectors, String, Hash Maps
Arrays don’t have a lot of useful functionality, so Rust has the Vector type that grows automatically and has some methods:
let mut v = Vec::new(); // define a new mutable vector
v.push("a value"); // push a value to our vector v
v.push("another value"); // push another value to our vector v
for item in &v { // iterate over v and print it's items
println!("{}", v);
}
let v = vec![1, 2, 3]; // there's a macro you can use to create vectors quickly
Rust has a String type included in the standard library. It’s UTF-8 encoded, and simple to use:
let mut str_empty = String::new(); // create a new empty string
let from_str = String::from("something "); // create a string from a string literal
str_empty.push_str("empty");
let something_empty = from_str + & str_empty;
println!("{}", something_empty); // prints out "something empty"
let som = &something_empty[0..2]; // take a slice of a string
println!("{}", som); // prints out "so"
Actually, Rust strings are somewhat innovative and a bit complicated. So you might want to read more about them. The Rust implementation include terms like “scalar values” and “grapheme clusters”. Spooky stuff.
The Hash Map type is pretty common, some languages call it dictionary.
let mut launch_codes = HashMap::new(); // define a new hash map
launch_codes.insert(String::from("BigRocket"), 101); // add values
launch_codes.insert(String::from("AtomicRocket"), 99181);
for (key, value) in &launch_codes {
println!("{}: {}", key, value); // iterate and print
}
Generics
Rust’s Generics reminds me of Java’s Generics.
Here we declare a vector of strings:
let strings: Vec<String> = Vec::new()
Struct definition with Generics:
struct TableCell<T> {
value: T
}
Here the value property can be of any type.
Traits/Interfaces
Traits are like Interfaces, but not exactly. You can implement a Trait on any struct, and accept parameters that implement a certain Trait. You can also define default behavior, kind of like a parent class.
Here is a definition of a Trait:
pub trait Pluginable {
fn plug_in(&self);
}
As you can see we are only defining a method without a body.
We can implement the Trait on a struct like this:
struct Wire {
wire_type: String
}
impl Pluginable for Wire {
fn plug_in(&self) {
println!("Wire is plugged in");
}
}
We can change Pluginable to have a default behavior:
pub trait Pluginable {
fn plug_in(&self) {
println!("plugged in")
}
}
We can accept parameters that implement a Trait with Generics:
fn plug_it_in<T: Pluginable>(something: T) {
something.plug_in();
}
In this example, T must be a struct that implements the Pluginable trait.
Closures
Closures are the equivalent of anonymous functions. They look like this:
let i_am_a_closure = |var| {
println!("{}", var);
};
Enums
Enums are pretty straight forward:
enum MachineState {
Working,
Crunching,
Stopped
}
We’ll talk more about enums in a bit.
The Cool Stuff
How Rust is different from other programming languages.
Mutability
We mentioned before that Rust’s variables are immutable by default. This is a design choice. To me it seems like saying: “I’ll check that your variables doesn’t change unless you explicitly tell me otherwise”. You can declare a variable as mutable with the keyword mut. It’s pretty straight forward:
let a = 5; // immutable
let mut b = 6; // mutable
a = 2; // can't do it
b = 10 // no problem!
Of course, there’s a whole chapter explaining it in “The Book”
Cargo - Not Just A Package Manager
Cargo is Rust’s command line tool, here is what you can do with it:
- Create a new project or library
- Build your project
- Run your project
- Run the tests for your project
- Manage dependencies and versions
- Install libraries from crates.io
- Publish libraries to crates.io
- Extend it with custom commands
I really like the approach of having an all in one cli. You can read more about it in the cargo guide.
Memory Management
Rust puts an emphasis on memory management. From the Rust FAQ:
“One of Rust’s key innovations is guaranteeing memory safety (no segfaults) without requiring garbage collection.”
Let’s see how it’s implemented.
Ownership
Ownership is Rust’s key feature, it manages memory with a set of rules that are enforced at compile time.
Here are the ownership rules as they appear in <a href=“
https://doc.rust-lang.org/book/second-edition/ch04-01-what-is-ownership.html" target-"_blank”>“The Book”:
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Let’s see an example:
{
{
let my_string = String::from("a string");
println!("{}", my_string);
} // my_string is now cleared
}
What happens when we assign a value to a new variable? Since that there can only be one owner at a time, values that live on the stack (such as int) will be copied, and values that live on the heap will be moved. Let’s see it in code:
let a = 9;
let b = a; // both variables have the value 9, hunky dory
let my_string = String::from("a string"); // a string will be allocated on the heap
let another_string = my_string; // the pointer value will be moved here,
// and my_string will no longer be valid
println!("my_string is {}", my_string); // will throw a compiler error
// something like: used of moved value: my_string
Wait what? But why? Well.. If we pointed twice to the same location in memory, that location would have been cleaned twice when the scope ended. I am guessing that the question you are asking yourself is “can I do reference counting?”. The answer is yes, but we’ll get to that later when we talk about Smart Pointers.
When you pass a parameter to a function, the same rules apply:
fn main() {
let my_string = String::from("a string");
move_there(my_string); // my_string is moved to the move_there scope
// my_string is no longer valid
}
fn move_there(your_string: String) { // the function move_there
println!("{}", your_string); // takes ownership of your_string
} // your_string (which was before my_string, hehe)
// is now dropped and it's memory cleared
A function can also give ownership:
fn gives_string() -> String {
let take_string = String::from("take it");
take_string // take_string will be returned and moved to
// whoever calls this function
}
As you might be thinking, this is very limiting. Luckily, you can also pass by reference! In Rust it’s called borrowing.
Can I Borrow Your Pointer Please?
Rust doesn’t allow “dangling pointers”. The compiler will make sure that if something holds a reference to a value, that value will be valid. This also means that there’s no null.
Let’s look at the borrowing rules from “The Book”:
- At any given time, you can have either but not both of:
- One mutable reference.
- Any number of immutable references.
- References must always be valid.
Let’s see an example of borrowing:
fn main() {
let a_string = String::from("its mine");
borrow_and_print(&a_string); // we pass a reference to a_string with &
println!("this is my string: {}", a_string); // a_string is still valid because
// it is borrowed instead of moved
}
fn borrow_and_print(your_string: &String) { // we accept a reference with &String
println!("this is your string: {}", your_string);
}
Let’s see an example of a mutable borrowing:
fn main() {
let mut a_string = String::from("its mine");
borrow_and_change(&mut a_string); // we pass a mutable reference to a_string with mut&
println!("this is my string: {}", a_string); // prints:
// "this is my string: its mine, but i changed it"
}
fn borrow_and_change(your_string: &mut String) { // we accept a reference with &mut String
your_string.push_str(", but I changed it");
}
You can’t mix when you borrow. If you borrowed a variable as immutable, you can’t borrow it as mutable even if it’s originally mutable!
let mut a = String::from("change me");
let b1 = &a; // I can have one!
let b2 = &a; // I am on a roll!
let b3 = &mut a; // can't do that though :(
Really? No Null?
No, it’s not a typo - it’s a design choice. As we stated before, references are always valid but there are cases where values are just not available. These cases are solved with the Option<T>
enum provided by the standard library.
The definition looks like this:
enum Option<T> {
Some(T),
None,
}
So, how should we use this marvelous enum instead of null?
Rust has a powerful control flow operator called match that can help us. It’s kind of like a switch case, but a bit different. The main difference is - you’ll have to match all the possible patterns, or have a default that catches all the patterns you didn’t cover. Otherwise, you’ll get a (you guessed it) compile error.
You can use match with the Option<T>
enum like this:
let might_be_none = a_function_that_returns_option(); // returns type Option<String>
match might_be_none {
Some(s) => println!("It's not none! it's {}", s),
None => println!("Omg it's None!"),
}
The match operator can also return a value:
let might_be_none = a_function_that_returns_option(); // returns type Option<String>
let my_value = match might_be_none {
Some(s) => s, // return the value in might_be_none to my_value
None => String::from("default_value"), // return a default value in
// case might_be_none is none
};
Lifetimes
So now that you have an idea of how ownership and borrowing works, let’s see what lifetimes are.
Each reference in Rust has a lifetime, but it’s mostly implicit. A lifetime is the scope for which a reference is valid. There are some cases where we need to explicitly declare the lifetime of a reference. Let’s look at the longest_function example from “The Book” - it’s a function that accepts two strings and returns the longest (doesn’t compile):
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x // we might return this reference
} else {
y // we might return this reference
}
}
As we said before - the compiler will make sure that if something holds a reference to a value, that value will be valid. In this case, the compiler is not sure what the lifetime of the return value should be and needs our lifetime annotation. A lifetime annotation looks like this: 'name_of_lifetime
The longest function with lifetime annotations would look like that:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
The signature now says that the parameters x and y have the lifetime 'a
and that the return value must have the same lifetime 'a
. This will make the compiler enforce that our return value will live at least as long as x and y.
This is just a taste of course, there’s more to learn about lifetimes in the lifetimes chapter.
Smart Pointers
If smart pointers weren’t a part of the standard library, you would have probably needed to implement them yourself.
For example, Boxing is a well known concept in computer science.
Rust has a Box<T>
type that you can use in case you need to do boxing. Want your int allocated on the heap? No Problem:
let b = Box::new(10);
In case you need a variable to have multiple owners, Rust offers the Rc<T>
type. The Rc type is a reference counting smart pointer, intended for single threaded use.
Here is an example of how you can work with it:
use std::rc::Rc;
struct Person {
first_name: String,
last_name: String
}
fn main() {
let p = Rc::new(Person{ first_name: String::from("turtle"),
last_name: String::from("techie")});
println!("Reference count is = {}", Rc::strong_count(&p)); // only 1 reference
{
let p2 = Rc::clone(&p); // reference no. 2 in another scope
println!("Reference count is = {}", Rc::strong_count(&p));
{
let p3 = Rc::clone(&p2); // reference no. 3 in yet another scope
println!("Reference count is = {}", Rc::strong_count(&p));
} // here p3 will be dropped so the count should get down to 2
println!("Reference count is = {}", Rc::strong_count(&p));
} // here p2 will be dropped so the count should get down to 1
println!("Reference count is = {}", Rc::strong_count(&p));
}
This program’s output will be:
Reference count is = 1
Reference count is = 2
Reference count is = 3
Reference count is = 2
Reference count is = 1
There’s also Arc<T>
which you might have guessed, is a thread safe implementation of the same pointer.RefCell<T>
implements the “interior mutability pattern”. This pattern enables you to enforce the “borrowing rules” at runtime instead of compile time. It uses the unsafe
keyword to bend the borrowing rules.
Break The Rules With Unsafe Code
Rust lets you bend the rules whenever you need to, it’s called “Unsafe Superpowers”.
Since Rust is a low level language, it’s understood if you need to read the contents of some arbitrary memory address, or call C code.
Here is what you can do with the unsafe
keyword (from “The Book”):
- Dereferencing a raw pointer
- Calling an unsafe function or method
- Accessing or modifying a mutable static variable
- Implementing an unsafe trait
There’s a whole book about unsafe rust called “The Rustonomicon”, funny right?
Concurrency In Rust Is “Fearless”
Rust has threads, they are actual threads as opposed to “green threads”. The syntax is similar to what you have seen in other programming languages:
let handle = thread::spawn(|| {
// thread code
});
handle.join(); // blocks the main thread until our spawned thread finishes
Message passing is a popular approach for sharing data between threads. Like Go, Rust offers channels. Here is an example from “The Book”:
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
Sharing state across threads is another popular approach. In this case, Rust offers Mutex<T>
.
Check out this example from “The Book” using Mutex<T>
and Arc<T>
:
use std::sync::{Mutex, Arc};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
So, is it really fearless? Well, you can still get deadlocks, which I find quite intimidating. I admit that my experience with concurrency in Rust is not enough for having a real strong opinion, but it looks like Rust’s ownership rules and memory safety features, would help move most errors to compile time, and that for me, is a big plus.
Give Me Some Of That Syntactic Sugar!
Rust has some nice syntactic sugar that makes code just a bit more readable. You must have noticed, there are macros:
println!("whatever");
let my_vector = vec![1, 2, 3];
You can limit a generic parameter to only accept structs that implement a trait with the where
keyword:
fn print_something<T>(t: T) -> i32
where T: Display // T must implement the Display trait for this to compile
{
// function body
}
You can define aliases with the type
keyword:
type Short = Box<Send + 'static>;
There’s also associated types - which are more than just sugar. It allows you to have a placeholder in a trait, as opposed to a generic parameter. Let’s look at this example from “The Book”:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
And the implementation:
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
Here, the struct implementing the trait declares it’s item type is u32.
Rust also has attributes. For example, the test attribute marks tests so they won’t be compiled when you build your code, but will be compiled when you test it. This is how it looks like:
#[test]
fn my_unit_test() {
// unit test code
}
Tests And Documentation
Rust supports unit tests, integration tests and documentation tests.
Unit tests are used to test internal parts of your library, while integration tests are aimed at the public api of your library.
Documentation tests are tests that you can write as part of your documentation, but you can totally run them! Check out this example:
//! The `adder` crate provides functions that add numbers to other numbers.
//!
//! # Examples
//!
//! ```
//! assert_eq!(4, adder::add_two(2));
//! ```
/// This function adds two to its argument.
///
/// # Examples
///
/// ```
/// use adder::add_two;
///
/// assert_eq!(4, add_two(2));
/// ```
pub fn add_two(a: i32) -> i32 {
a + 2
}
The two asserts will run as tests when you’ll execute cargo test, and will be counted as “doc tests”.
Rust’s documentation supports markdown, so you can write your documentation inside the source code and then use the cargo doc command to generate html documentation. Nice, right?
Summary
I find Rust very interesting. It’s definitely different from what I have seen so far. I haven’t written any “real” Rust code yet and I hope I’ll get the chance to use Rust for production in the near future.
If you liked what you read here and looking for where to go next, I believe reading the Rust book thoroughly is your best bet.
I hope you enjoyed, and of course, feel free to comment or share.
Thank you for reading.