'Introduction to the Rust Programming Language' Workshop from Frontend Masters
What Can I Build with Rust?
- web servers
- CLIs
- native desktop apps
- in-browser apps via WebAssembly
- performance-intensive libraries
- operating systems
Why to use?
- Speed, Performance, Going Real Fast
- like an upgrade to C and C++
- has better error messages
- nice ergonomics and a language server
- (mostly) automatic memory management
- a package manager and code formatter
- more compiler help with concurrency
- lots of compiler help for big code bases
- "most loved programming language" 2016-2020 on stackoverflow
Why not to use?
- big language - lots to learn!
- smaller ecosystem than C/C++ (but it has an FFI-Foreign Function Interface)
- slower iteration cycle than most languages
- strict compiler
- satisfying ("fighting") the borrow checker
- slow compile times for full builds
- tests can take a while to build
- safer than C++, but less safe than pure FP
Misc.
- compiles to either machine code or web assembly
- can't compile for different systems, with out compiling on that system (i.e. no compiling for windows from mac and vice versa)
- no classes, prototypes, or inheritance
- can create your own iterables and define how to iterate
- in general, Rust prefers to give errors at compile time rather than runtime
- doesn't have an equivalent of JS's
null
orundefined
PART 1 : PRIMITIVES
Strings
Hello, World!
fn main() { println!("Hello, World!"); // "Hello, World!" }
- put this in a fie
app.rs
and compile it byrustc app.rs
and it will generate a binary executable calledapp
, which you can run to see the output
- put this in a fie
Defining Variables with let
- all variables in rust are declared with
let
--- do not need to initialize it when it is declared, but must be initialized before it's used
fn main() {
let greeting = "Hello";
let subject = "World";
println!("{}, {}!", greeting, subject); // "Hello, World!"
}
println!
uses string interpolation -- replaces first{}
withgreeting
and second{}
withsubject
- string interpolation can't be used everywhere
format!
- can also use string interpolation
- returns the string after making any
{}
substitutions
fn main() {
let subject = "World";
let greeting = format!("Hello, {}!", subject);
}
Crashing with panic!
- can also use string interpolation
- used to intentionally crash a program (no try/catch in Rust so it will really crash) => nothing after the
panic!
will run
fn main() {
let crash_reason = "Server wanted a nap.";
panic!("I crashed! {}", crash_reason);
println!("This will never get run.");
}
Floats
println!
also works with numbersfn main() { let x = 1.1; let y = 2.2; println!("x times y is {}", x * y); }
- output is
x times y is 2.4200000000000004
because these are IEEE-754 binary floating-point numbers
- output is
- dividing by 0, gives infinity (not panic)
Mutability
let
works, by default, like JS'sconst
-- immutable- can use
let mut
to make it mutable (less common)
Numeric Types
- type errors given at compile time
- statically type-checked language --- every value has a single type associated with it at compile time and that type can never change at runtime
- use type annotations to specify exactly which types the values have
fn main() { let x: f64 = 1.1; let y: f64 = 2.2; println!("x times y is {}", x * y); }
- says that
x
andy
both have typef64
(i.e 64-bit floating-point number)
- says that
- Rust has type inference --- if it is unable to infer the type it will ask you to annotate the type at compile time --- will never silently infer the wrong type
Functions
- when writing functions must specify types of inputs and return --- not inferred
fn main() { println!("1.1 times 2.2 is {}", multiply_both(1.1, 2.2)); } fn multiply_both(x: f64, y: f64) -> f64 { return x * y; }
main
has no arguments and no returnmultiply_both
takes twof64
arguments and returns anf64
value
- if it ends in
!
it is a macro, not a function- macros can use string interpolation, functions can't
- can't be passed around like functions --- Rust has first class functions
- click here to read about macros
Sizes
f32
- 4 bitsf64
- 8 bits => more memory used => allows for more precision, but may also slow down the program
Integers
- can use
_
where a comma would be in a number --- i.e.1000
can be written as1_000
- dividing by 0 causes a panic
Sizes
- signed:
i8
- 8 bits (1B), -127 to 128i16
- 16 bits (2B), -32,768 to 32,767i32
- 32 bits (4B), ...i64
- 64 bits (8B), ...i128
- 128 bits (16B), ...
- unsigned:
u8
- 0 to 255u16
- 0 to 65,535u32
- 0 to 4,294,967,295u64
- 0 to 18,446,744,073,709,551,615u128
- 0 to 170,141,183,460,469,231,731,687,303,715,884,105,728char
- au32
that's been Unicode validated
- packages are available for arbitrary ints, but shouldn't really need anything bigger than
i128
oru128
Converting Numbers with as
as
can only be used on numeric types- if try to use different types interchangeably you get a type mismatch error at compile time
fn main() { let x: f64 = 1.1; let y: f32 = 2.2; println!("x times y is {}", x * y); // ERROR: mismatched types! }
fn main() { let x: f64 = 1.1; let y: f32 = 2.2; println!("x times y is {}", x * y as f64); // x times y is 2.4200000524520875 }
f32
->f64
takes more memoryf64
->f32
can result in information loss- =>
f64
->f32
->f64
can result in a different value than you started out with
- =>
- can't mix signed and unsigned integers
fn multiply(x: i64, y: u8) -> i64 { return x * (y as i64); } fn divide(x: i32, y: u16) -> f64 { return x as f64 / y as f64; }
Booleans
- can use
as
true as u8
evaluates to 1false as u8
evaluates to 0
1 == 2
evaluatesfalse
- no truthiness in rust -- only strict true and false
Conditionals
- syntax uses
if
/else
/else if
with{}
if cats > 1 { println!("Multiple cats!"); } else if cats > 1_000 { println!("Too many cats!"); } else { println!("Need more cats!") }
if
can be used like a ternary, no separate syntaxlet suffix = if same_name_as_parent_and_grandparent { "III" } else if same_name_as_parent { "Jr" } else if same_name_as_child { "Sr" } else { "!" }; println!("My name is {}{}", name, suffix);
- must contain an
else
as a catch all in case all previous conditions are false --- won't compile without it
- must contain an
- can write on one line if short
let result = if a > b { a } else { b };
Statements and Expressions
- an expression evaluates to a value
cats > 1000
- a statement does NOT evaluate to a value
println!("Sooo many cats!");
- statements terminate with
;
- statements terminate with
- if last line in function is an expression, automatically returned (must follow statements --- if multiple expressions then the
return
is needed) - comparison:
expression
statement
diffs:
PART 2: COLLECTIONS
- everything in this part is copy by value
Tuples
- a collection of 2 or more values (except for unit, see next section)
let point: (i64, i64, i64) = (0, 0, 0);
- can have mixed types and more or less values
- assign values from the tuple with name and index
or with destructuringlet x = point.0; let y = point.1; let z = point.2;
let(x, y, z) = point;
- can partially destructure by using underscores as placeholder for parts don't care about
let (x, y, _) = point;
let (x, _, _) = point;
let (_, y, _) = point;
- can partially destructure by using underscores as placeholder for parts don't care about
- can use
let mut
to make a mutable tuplelet mut point: (i64, i64, i64) = (0, 0, 0); point.0 = 17; point.1 = 42; point.2 = 90;
- tuples can't change size at runtime
Unit
- the zero tuple --- has nothing inside of it
let unit: () = ();
- used as a return for functions with no return value because every function is required to have a return value (no
void
in Rust) --- if nothing useful to return then by convention it should return()
--- e.g.main
andprintln!
both return()
Structs
- syntax sugar for tuples --- identical at runtime (i.e. no performance tradeoffs) --- difference is that at compile time struct elements are referenced by name rather than by position
- same as point in tuple example, but now the elements are named
struct Point { x: i64, y: i64, z: i64, }
- examples of how we could construct a new Point:
orfn new_point(x: i64, y: i64, z: i64) -> Point { Point { x: x, y: y, z: z } // or with syntax sugar: Point { x, y, z } }
let point = Point { x: 1, y: 2, z: 3 };
- examples of how we could construct a new Point:
- notice we now use
{}
instead of()
like we did with tuples - can also have mixed types
- can have mutable structs
let mut point = Point { x: 1, y: 2, z: 3 };
and can do things likepoint.x = 5;
- getting values out (using second new point example from above)
let x = point.x; // with struct name and element name
let Point { x, y, z } = point; // with destructuring
let Point { x, y: _, z } = point; // destructuring when don't care about y
let Point { x, z, .. } = point; // pretend all other fields are underscores
let Point { x, .. } = point;
- notice in second to last example, because they are named, the order doesn't matter
- can't change number of elements or names of elements at runtime
- possible to put functions in a struct, but not straight forward
Arrays
let mut years: [i32; 3] = [1995, 2000, 2005];
- NO mixed types --- all elements in example must be of type
i32
- fixed-length --- example has fixed length of 3 elements
- get values out
let first_year = years[0]; // by index/name --- can use other things as long as not out of bounds because // it will panic if out of bounds
let [_, second_year, third_year] = years; // with destructuring
- assigning
years[2] = 2010; // compile time check, never panic
years[x] = 2010; // runtime check (because it doesn't necessarily know what x will be) and can potentially panic
- can iterate (tuples and structs NOT iterable -- over values or fields)
for year in years.iter() { println!("Next year: {}", year + 1); }
- allowed to iterate because always know what the type will be (tuples can have mixed types)
Memory
Part 3: PATTERN MATCHING
Enums
- type goes after
enum
, then assign variants enum Color { Green, Yellow, Red, }
- can have Custom variants
Pattern Matching
match
works kind of likeswitch
in other languages- can use
match
as an expression (like withif
earlier) -- must cover every variant or use a catch all pattern or you will get a compiler error
Methods
************* functions vs methods? **************** ************* impl? ***************
- chainable
Self
Type Parameters
Option
- built into the standard library
enum Option<T> { None, Some(T), }
Result
Part 4: VECTORS
Vectors
- everything must be explicitly typed
Usize
Vectors vs Arrays
- can use a
for
loop with either - the tradeoffs here set the stage for the biggest factor in language performance because vectors have dynamic length--- comes down to heap vs stack memory
The Stack
Without a Return
With a Return
The Heap
Part 5: OWNERSHIP
Manual Memory Management
- rust doesn't ask us to manage memory manually
- it figures out when to deallocate memory so there are no 'use-after-free' or 'double free' issues
- use-after_free
- double free
- can cause safety issues
Automatic Memory Management
Garbage Collection
- most popular languages have this
- has more overhead
- can cause slowdowns while garbage collecting
How Rust Does It
- deallocates when goes out of scope
- if want to force it to deallocate earlier you can put things in stand alone
{}
-- stand alone scope for memory allocation
Ownership
- an 'owner' is assigned to a scope -- it can be transferred (called a 'move')
- this allows for the transfer ownership when you return something so the memory isn't deallocated when the function with the return completes
Use-after-move Compiler Error
- once you pass ownership it's gone, but you can assign to a new variable (rebind)
.clone()
- deep copy
- pass clone instead so original ownership is maintained -- avoids the use-after-move compiler error
- clones use more memory/sacrifices performance
Part 6: BORROWING
Borrowing
Mutable References
Restrictions
Slices
- doesn't own the elements, just references them