2 Language foundations
Last edited
Compiling
Compiling a single file with rustc
rustc ok.rs
./ok
# OKCompiling projects with cargo
Typically most projects are more than one file, we’ll use the higher-level tool than rustc, called cargo (cargo knows how to drive rustc)
Variable declaration
fnbegions a function definitionmain()is the entry point to all rust programsletto declare variable bindings, variables are immutable by default (read-only rather than read-write)println!a macro which is function-like but returns code rather than values"strings uses double quotes, single quotes forchar
fn main() {
let a = 10; // type infered
let b: i32 = 20; // declared
let c = 30i32; // type anno in literal
let d = 30_i32; // underscores do nothing, just readablility
let e = add(add(a, b), add(c, d));
println!("( a + b ) + ( c + d ) = {}", e);
}
// type declarations are REQUIRED for defining functions
fn add(i: i32, j: i32) -> i32 {
i + j // functions return the last expression result (no return needed)
}Numbers
All of these are stored as the infered default of i32
These are all just different ways to print/store, but they’re all the same resulting i32
fn main() {
let three = 0b11;
let thirty = 0o36;
let three_hundred = 0x12C;
println!("base 10: {} {} {}", three, thirty, three_hundred);
println!("base 2: {:b} {:b} {:b}", three, thirty, three_hundred);
println!("base 8: {:o} {:o} {:o}", three, thirty, three_hundred);
println!("base 16: {:x} {:x} {:x}", three, thirty, three_hundred);
}Rust types for scalar (single) numbers:
| Type | Meaning |
|---|---|
i8–i64 | Signed integers, 8–64 bit |
u8–u64 | Unsigned integers, 8–64 bit |
f32, f64 | Floats, 32/64 bit |
isize, usize | native-width int (64-bit on 64-bit CPUs) |
Comparsion
To compare two different types you have to use as to cast one operand to the other’s type.
let a: i32 = 10;
let b: u16 = 100;
if a < b {
...
}
if a < (b as i32) {
...
}Warning
Don’t cast carelessly!
300_i32 as i8returns44
Traits are methods on a type, but they must be brought into the scope to use the method.
Note
There are a handful of default methods and functions in the local scope: https://doc.rust-lang.org/std/prelude/index.html
Assert
// Crashes the program if not true
assert!(0.1 + 0.2 == 0.3);
Crate
usepulls crates into the local scope::namespace operator::restricts what’s imported, in this case a single typeComplex
use num::complex::Complex;
// Python version
from num.complex import ComplexTwo ways to initialize non-primative data types:
// Literal syntax
let a = Complex { re: 2.1, im: -1.2 };
// New function - most types have this as a helper
let b = Complex::new(11.1, 22.2);
Iteration
for
most used, similar to Python
There are some rules about borrowing and references here..
for item in container {
// ...
}Python for _ in range
for _ in 0..10 {
// ...
}Warning
Avoid the python syntax of indexing using
ilet collection = [1, 2, 3, 4, 5];for i in 0..collection.len() { let item = collection[i]; // ... }This is slow and less safe, it’s not idiomatic.
while
Same as python
while samples.len() < 10 {
let sample = take_sample();
if is_outlier(sample) {
continue;
}loop
You could use while true but it’s not idiomatic, use loop
loop {
// ...
}continue / break
continue, as you’d expectbreakAs you’d expect, only diff is break can return a value and jump to a loop label likegoto
I really like this
let n = loop {
break 123;
};
println!("{}", n);Conditional Branching
Strange - you can walrus a conditional block
let n = 123456;
let description = if is_even(n) {
"even"
} else {
"odd"
};
println!("{} is {}", n, description);match can make this even terser
let description = match is_even(n) {
true => "even",
false => "odd",
};Match
match item {
0 => {}, // single value
10 ..= 20 => {}, // range
40 | 80 => {}, // either OR
_ => {}, // matches every value (catch all)
}Example
let needle = 42;
let haystack = [1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862];
for item in &haystack {
let result = match item {
42 | 132 => "hit!",
_ => "miss",
};
if result == "hit!" {
println!("{}: {}", item, result);
}
}References
A reference is a value that stands in place for another value.
ais a variable storing a large array that is costly to duplicate.- A reference variable,
r, is a cheap copy ofa. Instead of creating a duplicate, the program storesa’s address in memory. - When the data from
ais required,rcan be dereferenced to make a available.
let a = 42;
let r = &a; // reference to a (address)
let b = a + *r; // derefence (get the value of the address in r)
// a + a = 84
Working example
let needle = 203;
let haystack = [1, 1, 2, 5, 15, 52, 203, 877, 4140, 21147];
for item in &haystack { // reference to haystack (cheap), meaning items are addresses
if *item == needle { // we now need to dereference item
println!("{}", item);
}
}Advanced func definitions
Lifetime annotations
Lifetime annotations allow programmers to declare their intent.
Don’t full get why you bother? Like of course it will live for the full function?
“It’s common to see lifetime parameters when using references.”
fn add_with_lifetimes<'a, 'b>(i: &'a i32, j: &'b i32) -> i32 {
*i + *j
}
fn main() {
let a = 10;
let b = 20;
let res = add_with_lifetimes(&a, &b); // references to a and b
println!("{}", res);
}<'a, 'b>declares two lifetime variables,'aand'bin the scope of the func
lifetimeaand lifetimeb.i: &'a i32binds lifetime variable'ato the lifetime ofi
“parameter i is a reference to an i32 with lifetime a.”
Generic Functions
Capitial lets indicate a generic type, conventionally T, U, V
fn add<T>(i: T, j: T) -> T {}Traits are interfaces, every operator has a trait.
Same as go seemingly, this says T must implement the ops::Add.
fn add<T: std::ops::Add<Output = T>>(i: T, j: T) -> T {
i + j
}Note
All rusts operators are syntactic sugar for a trait’s method. During compliation
a + bcoverts toa.add(b)
grep
lines() returns an iterator over quote where each iteration is a `line of text. Rust uses each operating system’s conventions on what constitutes a new line.
let search_term = "picture";
let quote = "\
Every face, every shop, bedroom window, public-house, and
dark square is a picture feverishly turned--in search of what?
It is the same with books.
What do we seek through millions of pages?";
for line in quote.lines() {
if line.contains(search_term) {
println!("{}", line);
}
}String and str
Stringuses dynamic memory allocation to store the text that it represents.&strvalues avoids a memory allocation.Stringis closer to a Python string with it’s methods and operations.&stris closer to an array of chars.Stringis read-write&stris read-only
String literals are &str
Lists
Lists are common: The two types that you will work with most often are arrays and vectors.
Arraysare fixed-width and extremely lightweight.Vectorsare growable but incur a small runtime penalty because of the extra bookkeeping
Arrays
- must be the same thing
- size cannot change
- items can be replaced
Creating/using arrays
- typing hiting with
[T; n],Telements type,nnumber of items
fn main() {
// Defining arrays
let one = [1, 2, 3];
let two: [u8; 3] = [1, 2, 3];
let blank1 = [0; 3];
let blank2: [u8; 3] = [0; 3];
// Nesting arrays (type inference)
let arrays = [one, two, blank1, blank2];
// Looping over reference of arrays
for a in &arrays {
print!("{:?}: ", a);
// Looping over the container
for n in a {
print!("\t{} + 10 = {}", n, n + 10);
}
// Looping over the length and indexing
let mut sum = 0;
for i in 0..a.len() {
sum += a[i];
}
println!("\t({:?} = {})", a, sum);
}
}Slices
Slices are dynamically sized array-like objects. The term dynamically sized means that their size is not known at compile time.
Because their compile-time size it not known, we use [R] rather than [T; n].
The distionction between array and slices isn’t vastly important in practice.
Vectors
Vectors (Vec<T>) are growable lists of T. Using vectors is extremely common in Rust code.
Final Grep Example
use clap::{App, Arg};
use regex::Regex;
use std::fs::File;
use std::io;
use std::io::BufReader;
use std::io::prelude::*;
fn process_lines<T: BufRead + Sized>(reader: T, re: Regex) {
for (i, line_) in reader.lines().enumerate() {
let line = line_.unwrap();
let line_num = i + 1;
match re.find(&line) {
Some(_) => println!("{}: {}", line_num, line),
None => (),
}
}
}
fn main() {
let args = App::new("grep-lite")
.version("0.1")
.about("searches for patterns")
.arg(
Arg::with_name("pattern")
.help("The patternt to search for")
.takes_value(true)
.required(true),
)
.arg(
Arg::with_name("input")
.help("File to search")
.takes_value(true),
)
.get_matches();
let pattern = args.value_of("pattern").unwrap();
let re = Regex::new(pattern).unwrap();
let input = args.value_of("input").unwrap_or("-");
if input == "-" {
let stdin = io::stdin();
let reader = stdin.lock();
process_lines(reader, re);
} else {
let f = File::open(input).unwrap();
let reader = BufReader::new(f);
process_lines(reader, re);
}
}