Rust was designed to give the programmer as much direct control as C++, allowing:
...but without all the bugs that plague C++ programmers. Rust is safe! There are:
Rust compiles to the metal and doesn't need a runtime nor a garbage collector, so:
...although:
But if you do need unsafe features, they are available:
Impressive, right? A lot of people think so. From the StackOverflow 2022 Developer Survey:
Rust wins the “most loved“ category — by quite a bit.
Here’s Jeff’s introduction:
Rust is a systems programming language, so you compile source files into machine executables. Say hello:
fn main() { println!("hello"); }
While yes, yes, you can run this online at The Rust Playground, TIO, or Replit, you’ll probably want to install Rust yourself and run it on the command line:
$ rustc hello.rs && ./hello hello
Like C, C++, and Go, you need that darn main
.
Here is what simple for-loops, if-statements, and string formatting look like:
fn main() { for c in 1..41 { for b in 1..c { for a in 1..b { if a * a + b * b == c * c { println!("({a}, {b}, {c})"); } } } } }
$ rustc triple.rs && ./triple (3, 4, 5) (6, 8, 10) (5, 12, 13) (9, 12, 15) (8, 15, 17) (12, 16, 20) (15, 20, 25) (7, 24, 25) (10, 24, 26) (20, 21, 29) (18, 24, 30) (16, 30, 34) (21, 28, 35) (12, 35, 37) (15, 36, 39) (24, 32, 40)
Another one. Here is Phil Dorin's 180° clock hands problem:
fn main() { for i in 0..11 { let t = (((i as f64) + 0.5) * 43200.0 / 11.0) as i32; let (hours, remaining_seconds) = (t / 3600, t % 3600); let (minutes, seconds) = (remaining_seconds / 60, remaining_seconds % 60); println!("{:02}:{minutes:02}:{seconds:02}", if hours == 0 {12} else {hours}); } }
$ rustc clockhands.rs&& ./clockhands 12:32:43 01:38:10 02:43:38 03:49:05 04:54:32 06:00:00 07:05:27 08:10:54 09:16:21 10:21:49 11:27:16
Yes, that conditional expression (if hours == 0 {12} else {hours}
) really looks like an if-statement. And yes, that is how it is written. There is no ?:
operator.
One more introductory example. Here is how to use command line arguments. And vectors. And result objects. And a lot of other stuff. Don’t worry about what this program does right now. Just get a feel for what Rust “looks like” for now.
fn main() { let args: Vec<String> = std::env::args().collect(); if args.len() != 2 { println!("Exactly one command line argument required"); return; } let Ok(n) = args[1].parse() else { println!("Must be positive integer"); return; }; let (mut a, mut b) = (0, 1); while b <= n { print!("{b} "); (a, b) = (b, a + b); } println!(""); }
$ rustc fib.rs && ./fib Exactly one command line argument required $ ./fib 20000 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 $./fib dog Must be positive integer
Now that we have seen what Rust programs look like, it’s time to learn (a lot of) the language. Where can we do this? Here are some of the important reads:
Getting advanced? Try:
Let’s take our own language tour now.
Introduce new variables with let
. If a variable is not used, that’s an error, unless the variable’s name starts with an underscore.
fn main() { let x = 1; println!("{x}"); // used itlet y = 2;// ERROR: y is not used let _z = 3; // Ok, don't have to use this one }
Subsequent declarations of variables with the same name shadow previous ones.
fn main() { let x = 1; // introduces a variable println!("{x}"); // prints 1 let x = 2; // completely different variable, shadows previous x println!("{x}"); // prints 2 }
Variables are immutable by default. Use mut
to make the variable mutable.
fn main() { let x = 1;x = 2;// cannot assign twice to immutable variable `x` println!("{x}"); // prints 1 let mut y = 3; // mut makes it mutable println!("{y}"); // prints 3 y = 5; // second assignment is ok println!("{y}"); // prints 5 }
Blocks create new scopes. Variables introduced in blocks are in scope only from their declaration to the end of the block.
fn main() { let x = 1; { println!("{x}"); // prints 1 (value of x from outside) let x = 2; // a completely new var, shadows outer x println!("{x}"); // prints 2 } println!("{x}"); // prints 1 (we are back outside) }
Rust always constrains the types of values allowed for a variable. Normally the constraint is inferred from the values assigned to it (including an initializer, if present). The constraints are checked at compile-time.
fn main() { let mut x = 2; // x constrained to an integer type println!("{x}"); x = 5; // assignment is ok! println!("{x}");x = 3.0;// mismatched types: expected integer found floating-point number }
If there is no initializer, you can’t use the variable until you’ve assigned to it. Then it gets its type constraint.
fn main() { let x; // Perfectly okprintln!("{x}");// used binding `x` is possibly-uninitialized x = 5; // Finally initialized constrained to integer type println!("{x}"); // Happily prints 5 }
You can explicitly supply a constraint even if an initializer is present.
fn main() { let a: i32 = 5; let b: i8 = 3; let c: char = '😀'; let d: [i32; 3] = [10, 20, -30]; let e: bool = false; let f: f64 = 3.8E-5; let g: String = String::from("hello"); println!("{a} {b} {c} {d:?} {e} {f} {g}"); // prints 5 3 😀 [10, 20, -30] false 0.000038 hello }
The let
statement introduces variables via patterns. Examples:
fn main() { struct Point { x: f64, y: f64 } let p = Point { x: 3.3, y: -1.0 }; let (a, b) = (3.5, "interesting"); let [c, d] = [false, true]; let Point {x: e, y: f} = p; println!("{a} {b} {c} {d} {e} {f}"); // prints 3.5 interesting false true 3.3 -1 let Point {x, y} = p; // x is short for x:x let [z, ..] = [1, 2, 3]; // .. stands in for "all remaining" println!("{x} {y} {z}"); // prints 3.3 -1 1 }
A pattern that might not match is called refutable. You can define with “let-else” to handle a refutable pattern.
fn main() { let mut v = vec![1, 2, 3]; let Some(t) = v.pop() else { return; }; println!("{t}"); // prints 3 }
There are basically three kinds of statements.
Kind | Purpose | Notes |
---|---|---|
Let Statements | Introduces one or more new variables | (Many examples above) |
Item Declarations | Declares an item | Items can be: modules, extern crates, use declarations, functions, type aliases, structs, enums, unions, constants, statics, traits, implementations, extern blocks, macros |
Expression Statements | Evaluates an expression and “throws away” the result, evaluating the expression only for its side effect(s) | Expression statements can be: literals, paths, oeprators, groups, arrays, awaits, indexings, tuples, tuple indexings, structs, calls, method calls, field accesses, closures, async blocks, continues, breaks, ranges, returns, underscores, macros, blocks, unsafe blocks, infinite loops, predicate loops (whiles), predicate pattern loops (while-let) iterator loops (for), ifs, if-lets, matches, labeled expressions |
It’s rather interesting that expressions are statements, and many things you normally think of as statements in other languages (if, while, match, etc.) are actually expressions.
There is some power in this idea. Blocks are sequences of statements, but the blocks themselves are expressions! The value of the block is the value of the last expression in the block, but be careful with semicolons: the empty expression is an expression, so you probably don’t want a final semicolon if you want to the block expression to have a value.
Blocks are actually expressions!
But if the last thing in a block is an expression, then the block itself is an expression, and the value of the block expression is the value of the last expression. Wild!
This works well for functions bodies, which are also blocks.
What types are available to you? A lot! Here are the built-in ones. All of them have values of a fixed size, so we know exactly how much space each of their values need at compile time. This is necessary to make programs run as fast as possible:
Type | Examples | Notes |
---|---|---|
i8 | 24i8 | -128...127 |
i16 | -30000i16 | -32768...32767 |
i32 | 99310855i32 | -2147483648...2147483647 |
i64 | -89i64 | -9223372036854775808...9223372036854775807 |
isize | 3500022isize | Signed integer with size of a pointer on the host architecture |
u8 | 255u8 | 0..255 |
u16 | 4095u16 | 0...65535 |
u32 | 999999999u32 | 0..4294967295 |
u64 | 2u64 | 0..18446744073709551615 |
usize | 89553421138usize | Unsigned integer with size of a pointer on the host architecture |
f32 | 3.2e-7f32 | 32-bit IEEE Float |
f64 | 1f64 | 64-bit IEEE Float |
bool | true | true or false
|
char | 'å' | 32-bit Unicode scalar value in range 0x0000–0xD7FF, 0xE000–0x10FFFF |
str | (Cannot write out values of this type) | Basically a [u8] , that is, a slice (see below) of 8-bit unsigned bytes, though whose elements constitute a valid UTF-8 sequence. (Like all slice types, you never see instances of this type written on their own.)
|
! | (This type has no values) | This is called the Never type. There are no values of this type. It is the return type of functions that don’t return.
|
And if you want to make your own types, there are a crazy amount of ways to do this. Here are a few of them:
Kind of type | Examples | Notes |
---|---|---|
tuple types | () (u16, char, f64) | |
array types | [i32; 5] | Yes, length is part of the type. [1,1,2,3,5]
|
slice types | [i32] | |
struct types | struct {x: f64, y: f64} | |
enum types | ||
union types | ||
function item types | ||
closure types | ||
pointer types | ||
trait object types |
Not only can you make your own types, but so have thousands of other programmers. These are available to you in various crates. Many of the crates are part of the Rust standard library. Here are a few:
TODO
Why so many types?As a safe, non garbage-collected, zero-abstraction systems language, Rust needs to have numeric types of known sizes, have explicit pointers, and make sure there are ways to control the sizes of what are normally thought of as dynamically sized entities like array slices and strings. Making sure everything is memory safe is handled in the language, often in the type system.
Tuples are cool! Access components with .0
, .1
, etc. Destructure as needed, too:
TODO
Structs are pretty similar to tuples, but while tuple components are positional, struct components are named.
Data is owned by default. But there is region-based borrowing. Individual types can be marked as copy types. And there are (C++-style) destructors.
The big problem is the combination of aliasing with mutation. Can we separate those things?
Rust programs are made up of crates. Each crate is a collection of items. Because creates can contain thousands of items, the items may be grouped into modules. The modules are arranged in a hierarchy within the crate. You can think of the the “top-level” of the crate as an unnamed module.
To refer to items, use ::
. Some examples from above:
crate1::item2
crate1::module1::item6
crate1::module1::module2::item5
crate1::module3::item7
But if you use the use
keyword, you don’t have to give the full path to the item.
TODO