🦀 Learning Rust
📖 Table of Contents
00 Getting Started: Cargo, Data Types and Core Language Features
Hello, my name is Tedd. I want to learn about Rust.
I’m a beginner. If you are too, perfect, stick around!
I hope you enjoy learning about this very compelling language.
🦀 About Rust
Rust is a programming language with high-level ergonomics and low-level control. The big things to draw attention to are:
- High Level Language, Low Level Performance - It’s “Blazingly Fast”
- Non-GC Memory Safety Ownership
- “Fearless Concurrency”
- Zero Cost Abstractions
Resources I am using on this journey:
Interesting Rust articles
Installing Rust
tl;dr
Mac
brew install rust
Windows
windows .exe fileCargo
Rust has a package manager called cargo
📦 cargo
is used to install “crates”
Community crates can be found at www.crates.io
Cargo isn’t just a package manager.
It’s will also compile your code, create new projects, generate documentation, run tests, publish your crates to crates.io and more.
Some common commands
cargo new # Creates new crate inside of brand new dir
cargo init # Initalizes a new crate in current dir
cargo build # Builds crate, compiles code
cargo run # Compile and run code
cargo add # Add a crate to dependencies
cargo doc # Generates crate documentation in an HTML page
📚 We won’t go into a ton of detail here - The Cargo Book is a good place to get yourself started.
Compiler
Speaking of compilers, let’s talk about rustc
real quick.
While Rust is indeed blazingly fast during runtime, compile time can be a different story as your project scales.
Rust is a robust and expressive language that places a strong emphasis on performance and correctness. The Rust compiler, known as rustc, is responsible for ensuring these qualities by performing several phases, such as lexing and parsing, type inference, and LLVM IR code generation. Rust’s statically-typed nature and ownership/borrowing system make the compiler’s job more complex, leading to longer compile times for larger projects.
Fortunately, the Rust community offers solutions such as sccache, a compiler caching mechanism that can significantly speed up compilation times.
For a deeper dive on how rustc
works, check out this overview of the Rust compiler.
🦀 Rust is not a panacea and compile times may come up as your project scales. Despite this, Rust’s focus on robustness and performance makes it a popular choice among developers who value correctness and efficiency - many feeling the trade off is more than worth it.
Community
The Rust community prides itself on:
“Empowering everyone to build reliable and efficient software”.
They are welcoming and kind to people of all skill levels.
🦀 A Rust developer is referred to as a “Rustacean”.
Syntax and Core Language Features
Variables, Constants and Mutability
Rust uses the let
keyword to declare variables.
Rust is statically & strongly typed - all values are immutable by default.
T
is used to represent generics.
Immutable by default
By default, all assigned values in Rust are immutable.
Declare as mutable using the mut
keyword.
let a = 5
a = 245 // immutable 🚫
let mut b = 5
b = 245 // mutable 🦀
🦀
mut
- pronounced “mute”, not “mutt”
Type annotation
Types are declared using the : T
syntax
In many situations, types can be infered by the compiler.
let a: i32 = 5; // Type explictly set as 32-bit int (i32)
let b: bool = 5; // Type explictly set as boolean (bool)
let mut c = "Fearless Concurrency"; // Type infered as a mutable string slice (&str)
let d = (32, "fifty seven", 83.2681) // Type infered as tuple (i32, &str, f64)
🌒 Shadowing
Rust variables can be overwritten by binding a new reference.
let c: i32 = 10; // initialize a value 🌖
let c: i32 = 20; // create a shadow reference 🌓
let c: i32 = 30; // as many times as needed 🌑
{
let c: i32 = 40; // shadows are block scoped
println!("{}", c); // > 40
}
println!("{}", c); // > 30
🦀
println!()
is no ordinary function invocation. It is a macro that prints text to the console.
We will cover what a macro is in a later post. For now all you need to know is the squiggle brackets{}
tell the macro “printc
here”
Shadowing vs. Mutability
Shadowing and mutable variables are two distinct concepts.
// Shadowing
let x = 5;
let x = "five"; // Shadows the previous x with a string
println!("{}", x); // > "five"
// Mutable reference
let mut y = 5;
y = 4 // mutates successfully
y = "five"; // error - type mismatch
Shadowing lets you define a new variable with the same name as an existing variable, hiding the original variable from the current scope without effecting it’s type or value.
Mutable variables, on the other hand, let you change the value of a variable, as long as it remains the originally declared type.
Block Scope
Variables in Rust are blocked scoped.
let d: i32 = 30; // immutable reference
{
let d: i32 = 44; // shadow a value from outer scope
let f: i32 = 55; // declare a new value inside scope
}
println!("{}", d); // > 30 - Shadowed value is no longer available
println!("{}", f); // > Error - f is not available in this scope
🌒 Notice the shadowed value is no longer in scope when we reach the bottom print statements. The lifetime of the inner reference to
d
has ended.
A Quick Word on Lifetimes
Rust uses the concept of lifetimes to provide memory safety without the need for a garbage collector.
In Rust, memory is managed through a system of ownership and borrowing, where every value has an owner and can be borrowed by other parts of the code. Owned references have a lifetime associated with them. This system ensures that memory is not freed prematurely or accessed after it has been freed.
⏳ We will have a detailed look at lifetimes in a future post, but for now just keep in mind that every reference in a Rust program has an owner and a lifetime.
Constants
Defined using the const
keyword.
Constants are computed at compile time, never at runtime
Constants are always immutable, must have a type declaration and can not be shadowed
// Can be declared in the global scope - This is not true of `let`
const MAX_SIZE: i32 = 100_000; // compile-time constant
fn main() {
println!("{}", MAX_SIZE); // > 100,000
}
⏳ The lifetime of a constant in Rust is for the entire duration of the program’s execution. Constants are a type of static variable, which means that they have a fixed address in memory.
Comments
Comments start with two slashes and go to the end of the line.
Block comments are also available via slash asterisk.
Scalar Data Types
Non-structured, singular values:
Booleans
|Unsigned Integers
|Signed Integers
|Floats
|Characters
Boolean
Ya got cha’ bools. One byte in size.
Denote with
: bool
Numbers
Unsigned Integer
|Signed Integer
|Floating Point
Rust gets you thinking about what type of number you are storing and how memory should be allocated accordingly. This is important for dialing in performance while preventing things like integer overflow.
Unsigned Integers (8-128 bit)
By not using the
-
signal, we have an extra bit in memory to work with.This bit allows us to count higher.
The trade off being this number must be a positive number.
Unsigned literally means, no sign.
Denote with
: u8 : u16 : u32 : u64 : u128 : usize
Signed Integers (8-128 bit)
The range that can be represented by a signed integer is symmetric around 0, with the negative and positive limits differing by one.
For example, an
i8
variable can represent values from-128
to127
Denote with
: i8 : i16 : i32 : i64 : i128 : isize
Floating Point
Floating point numbers are a way to represent real numbers in computing using scientific notation. In Rust, floating point numbers can be represented using either 32-bit
f32
or 64-bitf64
precision.Denote with
: f32 : f64
Strings
Strings are a more complex data structure than many languages will lead you to believe. While this can make our lives easier, it can also obfuscate important knowledge and impact performance.Rust surfaces some of this complexity and gets you thinking about strings from the computer’s perspective.
It is important to recognize that, at a certain level, strings are just an array of characters or bytes.
Rust has a few different string types:
: char
- 4 bytesCan represent any Unicode scalar value.
: &str
- String “slice”Implemented as a pointer to a contiguous sequence of UTF-8 encoded bytes
We will discuss strings in greater detail later in this series - for more immediate comprehensive knowledge I recommend checking out MIT's guide to strings in Rust.: String
- Heap-allocated, growable stringImplemented as a vector of bytes (Vec<u8>)
A quick word on UTF-8.
“ASCII-compatible variable-width encoding of Unicode, using one to four bytes”
It’s important to note that Rust strings are represented as UTF-8 encoded byte sequences, which means that each character in a string may actually be multiple bytes long.
This is different from languages like
Finally understanding unicode and utf-8C
andJava
, where each character is always a single byte. If you are unfamiliar (like I was), I would encourage you to spend an afternoon with the subject. It is useful to understand and will make working with strings in Rust, and other languages, much less opaque.What Every Programmer Absolutely, Positively Needs To Know About Encodings And Character Sets To Work With Text
Compound Data Types
Structured data comprising multiple values
Arrays
|Tuples
|Collections
|Slices
|Structs
Arrays
The pedantic definition of an array could be something along the lines: "a data structure that stores a collection of identically typed elements in contiguous memory locations."In this sense Rust arrays are true arrays.
Arrays in Rust have a fixed size in memory and must contain items of the same type.
Arrays in Rust are allocated on the stack and can not be grown.
Tuples
Tuples are also a list of values, but can have items of different types.
Like arrays, tuples in Rust have a fixed size in memory and can not be grown.
Vectors
Vectors are dynamic, heap-allocated data structures that can be grown.
Vectors can only store items of the same type.
Vectors are a data structure found in the
std::collections
module.Primitives vs. Collections
Things like
array
,str
,char
andtuple
are low-level language primitives, whereas vectors are “collections”. Collections are data structures found inside thestd
module that can grow dynamically, be allocated on the heap and do not have to have a known size at compile time.If you remember earlier we discussed the different string types. We suggested that a
String
is a implemented as a vector of bytesVec<u8>
. This means aString
is a collection, because it is avec
.Collection Types
Vec
|VecDeque
|HashMap
|LinkedList
|BinaryHeap
|HashSet
|BTree
&Slices
Create a “borrowed” view into a sequence of contiguous memory using
&[T]
Arrays, Strings and Vectors, can all be accessed via slices.
Specify the slice range with two dots in between delimiters inside square brackets
[1..5]
We can’t go too deep into the weeds in this guide, but you can read more about slices in Rust here.
&
ampersand syntaxThis is a key piece of syntax that exposes one of the most important concepts in all of Rust: “borrowing”.
In Rust, every piece of data has an “owner” that is responsible for managing that resources memory. This mechanism is ultimately what allows our programs to be memory-safe.
We can ask to “borrow” references to data from the owner. This is done by prefacing the reference to our data with an ampersand:
&ref
. You can borrow as many read-only references as you want. However if you can only make one mutable reference at a time.Structs
Structs allow us to group related data together.
In Rust this is the only way to contruct a user-defined type.
Define a struct using the
struct
keyword, followed by the name.Dynamic Behavior with
impl
We can add functionality to our types via the
impl
keyword.This is called an implementation block.
Functions you add here can be called using a method like syntax.\
Lifetime Annotation Syntax
<'_>
We’ve been slowy adding more and more syntax and now we have added angle brackets with a single quote and a character inside. This is how we annotate lifetimes in Rust.
Lifetimes are a mechanism for the compiler to ensure that any borrowed values are valid and don’t outlive the original value.
Lifetime annotations help prevent things like dangling references, segfaults and more. They are key to how memory is managed. More on lifetimes in the next article. If you want to go deeper now check out the Rust Book section on Lifetimes.
Traits
Traits are key to creating shared, polymorphic behavior in Rust.
Defines interfaces that different types can implement.
This allows us to write code that is generic and reuseable.
One of the unique characteristics of Rust is it’s trait system. Traits allow types to implement shared behavior, enabling polymorphism and providing a great interface for working with generics.
Traits are very important to the language and deserve a detailed explanation. We will have an entire post dedicated to the subject in the near future. Below is a dead simple program that makes use of some of what we have learned so far and some of what we will learn about shortly.
Functions
Function Declarations
The
fn
keyword is used to declare a new functionFunctions and their arguments are declared using
snake_case
🐍Parameters must have explicitly set types.
Rust programs specify a
main()
function as their entry point. This is an industry convention that goes way back and is used in many other languages. We don’t show it in every example, but it will be there in every Rust application you ever write.Return Types
The skinny arrow
->
indicates the return type.All
{}
blocks in Rust use their last expression as a return value.By ommiting a semicolon, we tell the compiler evalute this as an expression
Closures
Closures are the equivelant of an anonymous function.
Define using the
|params| body
syntax.Unlike normal function declarations, closure parameters can be infered.
Statements and expressions
Rust is an expression based language.
This means almost anything can evaluate to a value.
An expression is anything that evalutes to a value.
Functions and closures are expressions. As are control flow mechanisms like
if
andwhile
.This means we can assign things like
for
loops to values, so long as they guarantee to yield a value.Control Flow
operators
|conditionals
|loops
|pattern matching
|error
Arthimetic, Operators, Equality and Referencing
Conditionals
if
|else
|else if
Condition predicates must evaluate to boolean.
Rust has no concept of “truthy” or “falsey”.
If you recall, blocks
{}
use their last expression as a return value.This enables us to write concise, expressive code by setting control flow to a value and removing visual bloat from
return
keywords.Also note that you can not compare different types in Rust.
Other dynamic languages like Javascript will perform implicit type coercions, but not Rust.
Loops
loop
|while
|for
|break
|continue
The
loop
keyword gives us a way to perform execution until webreak
You can break out of nested loops with
loop labels
.A loop label is an identifier prefixed with
'
.The
while
keyword allows us to write loops that execute until a condition is met.The
for
keyword is used when iterating over a collection of elements.Iterators
Under the hood of
for
you will find the iterator pattern. Iterators are an extremely useful and powerful abstraction in Rust that take cognitive load of the developer and give a standardized pattern for looping over collections.We will be covering iterators in more detail through a follow up post. In the meantime I suggest you take a peek at the MIT Guide to Iterators in Rust.
Pattern Matching
Match against values using the
match
keyword.A sophisticated and concise syntax for testing multiple possible values.
Analagous to the
switch
statement in other languages.Every possible branch must be handled, otherwise your code will not compile.
Enums
Enums, or enumerations, are a powerful type construct that allow us to specify multiple variants for any given type. Rust’s enum system is incredibly powerful. When paired with pattern matching Rust’s compiler can enforce that we consider every possible branch (variant) of logic in our application. This means you are guarunteed no runtime crashses. Enum’s are so powerful they deserve a post of their own and will be covered in detail in a follow up post.
Error Handling
Result
|Option
|unwrap
|unsafe
Rust does not have exceptions.
Instead, Rust has a special enum called
Result
.It has two states
Ok
andErr
.Rust also has a way to handle
null
like values.There is another enum called
Option
.Option
can represent the possibility of a value being absent.It has two variants
None
andSome
.We can use the keyword
unwrap
to get back the value contained in ourOk(T)
. IfErr
is returned the program will panic. This is great for the ideation phase and prototyping, but you probably want to avoid usingunwrap
in your production code.There’s a bit more to it, but we have covered enough for now. We will explore error handling in more depth as this series progresses.
Wrap Up
This resource hopes to serve as a somewhat thorough, but brief overview of the high level language features, syntax and ideaoligies behind this interesting & important language. I tried to provide useful links along the way and hope this serves as a nice spring board for you into your Rust journey. We covered a lot, but this was to set you up with the building blocks we will begin to stack as we dive deeper into the Rust sea. Subsequent posts should be shorter in length overall, but will dive deeper into each compelling concept in the language.
Feelin’ Rusty yet? Today we learned:
cargo
This is a great first step toward becoming a Rustacean.
Let’s ramp things up a bit and talk about ownership and borrowing