2 Language foundations

Last edited

Compiling

Compiling a single file with rustc

rustc ok.rs
./ok 
# OK

Compiling 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

  • fn begions a function definition
  • main() is the entry point to all rust programs
  • let to 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 for char
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:

TypeMeaning
i8–i64Signed integers, 8–64 bit
u8–u64Unsigned integers, 8–64 bit
f32, f64Floats, 32/64 bit
isize, usizenative-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 i8 returns 44

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

  • use pulls crates into the local scope
  • :: namespace operator :: restricts what’s imported, in this case a single type Complex
use num::complex::Complex;
// Python version
from num.complex import Complex

Two 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 i let 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 expect
  • break As you’d expect, only diff is break can return a value and jump to a loop label like goto

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.

  • a is a variable storing a large array that is costly to duplicate.
  • A reference variable, r, is a cheap copy of a. Instead of creating a duplicate, the program stores a’s address in memory.
  • When the data from a is required, r can 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, 'a and 'b in the scope of the func
    lifetime a and lifetime b.
  • i: &'a i32 binds lifetime variable 'a to the lifetime of i
    “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 + b coverts to a.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

  • String uses dynamic memory allocation to store the text that it represents.

  • &str values avoids a memory allocation.

  • String is closer to a Python string with it’s methods and operations.

  • &str is closer to an array of chars.

  • String is read-write

  • &str is read-only

String literals are &str

Lists

Lists are common: The two types that you will work with most often are arrays and vectors.

  • Arrays are fixed-width and extremely lightweight.
  • Vectors are 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], T elements type, n number 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);
    }
}