This lab will introduce you to the logistics of programming in Rust. To get the starter code for this lab, go to the assignments repository and run git pull, then go into the lab2/starter directory.

Installation

First, you need to get Rust installed on your computer. Follow the instructions at rustup.rs to get the latest verison of the Rust compiler and its package manager, Cargo.

Then, install a Rust package for your favorite code editor: Vim, Emacs, Atom, Sublime, Visual Studio Code, IntelliJ.

You’re set up! Too easy.

Running code

Rust has no interpreter, so you’ll have to run code by compiling it and executing the resultant binary. You can do this in one of two ways. Option 1 is rustc:

$ cat test.rs
fn main() {
  println!("Hello, world!");
}
$ rustc test.rs
$ ./test
Hello, world!

Like gcc, this takes as input a Rust source file with a main function and outputs a binary that runs the main function. Your other option is Cargo:

$ cargo new foobar
     Created binary (application) `foobar` project
$ cd foobar
$ cargo run
   Compiling foobar v0.1.0 (file:///<PATH>/foobar)
    Finished dev [unoptimized + debuginfo] target(s) in 1.18s
     Running `target/debug/foobar`
Hello, world!

Cargo allows you to define “crates” (Rust projects) that have dependencies on other Rust code. In the example project, we now have a file foobar/Cargo.toml containing:

[package]
name = "foobar"
version = "0.1.0"
authors = ["Will Crichton <wcrichto@cs.stanford.edu>"]

[dependencies]

See crates.io for a full list of the publicly available Rust crates. Let’s say we want to get a random number using the rand crate. We can add rand = "0.5" under [dependencies] in the manifest file, change foobar/src/main.rs to:

extern crate rand;
use rand::prelude::*;

fn main() {
    println!("Random: {}", random::<u8>());
}

Then run:

$ cargo run
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading rand_core v0.2.2
 Downloading rand_core v0.3.0
   Compiling rand_core v0.3.0
   Compiling libc v0.2.43
   Compiling rand_core v0.2.2
   Compiling rand v0.5.5
   Compiling foobar v0.1.0 (file:///Users/will/Code/cs242-f18/site/labs/foobar)
    Finished dev [unoptimized + debuginfo] target(s) in 0.40s
     Running `target/debug/foobar`
Random: 213

Basics

Rust is OCaml, but with mutation and no default garbage collection. Seriously, you should internalize that statement as hard as you can. Rust has many features—it’s a complex language—but for the most part, you can see 75% of the features as directly mapping onto things you learned in OCaml. When you learn a new programming language, it’s an extremely valuable skill to explicitly build on your existing knowledge. You want to recognize when a language construct is not explainable in terms of your existing vocabulary (e.g. ownership in Rust), and focus on learning the essence of the new concepts.

Below, I quickly go through several code snippets to give you a sense of the syntax and style of Rust.

fn main() {
  // Variable binding.
  let n: i32 = 1;

  // Binding with type inference.
  let n = 1;

  // Variable shadowing (a la OCaml).
  let n = n + 1;

  // Mutable values.
  let mut n = 0;
  n = n + 1;

  // String literals are immutable pointers.
  let s: &str = "Hello world";

  // String objects you can mutate.
  let mut s: String = String::from("Hello ");
  s.push_str("world!");

  // Named product type (struct) type definition.
  struct Point { x: f32, y: f32 };

  // Struct constructor.
  let p: Point = Point { x: 1.0, y: 2.0 };

  // Struct accessor.
  println!("({}, {})", p.x, p.y);

  // Polymorphic named sum type (i.e. variant), or "enum" in Rust
  enum Option<T> { None, Some(T) }

  // Enum constructor.
  let opt: Option<i32> = Option::Some(1);

  // Match statements.
  println!("{}", match opt {
    Option::Some(n) => n,
    Option::None => -1
  });

  // Resizable array (vector).
  let mut v: Vec<i32> = Vec::new();
  v.push(2);
  v.push(3);

  // Fixed-size arrays.
  let mut arr: [i32; 4] = [0, 2, 4, 8];
  arr[0] = -2;
  println!("{}", arr[0] + arr[1]);

  // Slices (views into arrays/vectors)
  let mut slice: &[i32] = &arr[1..];
  println!("{}", slice.iter().sum::<i32>());

  // Iterators.
  for i in v.iter() {
    println!("{}", i);
  }

  // Infinite loops (while true).
  let mut i = 0;
  loop {
    i += 1;
    if i == 10 { break; }
  }

  // While loops.
  while i < 20 {
    i += 1;
  }
}

// Functions.
fn fib(n: i32) -> i32 {
  if n <= 1 { n } else { fib(n-1) + fib(n-2) }
}

Exercises, part 1

All exercises below are in src/part1.rs. Before you start, uncomment the line mod part1 in src/main.rs. You can run tests for the part by running cargo test.

  1. Implement a password checker, innovating on your design from last lab by printing out the number of guesses at each step until the password is guessed. For example, if the program is:

    fn main() { part1::password_checker(String::from("Password1")); }
    

    Then the desired interaction is:

    $ cargo test password_checker -- --nocapture
    Foobar
    Guesses: 1
    Password
    Guesses: 2
    Password1
    You guessed it!
    

    Note that for this test, you will need to run your test with the --nocapture flag to work.

  2. Implement two function that add a constant to each element of a vector. First, add_n returns a new vector, and second, add_n_inplace modifies the input in-place, returning nothing.

    You can either use for loops or the iterator API.

  3. Implement a function dedup that deduplicates a vector in-place. If an element is repeated anywhere in the array, the only the element with the smallest index should be kept. See test_dedup for example usage.

Pointers and ownership

fn main() {
  // Immutable and mutable references (or borrows).
  let mut n: i32 = 1;
  {
    let nptr: &i32 = &n;
    // n = 2; <- invalid!
  }
  n = 2; // ok!
  {
    let nptr: &mut i32 = &mut n;
    *nptr = 3;
    // n = 2; <- still invalid!
  }

  // Ownership.
  let mut s: String = String::from("Hello");
  s.push_str(" world!"); // Owner can mutate its value
  let s2 = s; // Pass off ownership
  // println!("{}", s); <- invalid! s has moved
  println!("{}", s2);

  let s4 = {
    let s3 = s2 + " Nice to see you again!";
    s3 // Pass ownership by returning from a scope
  };
  // Note that s, s2, s3 are all moved or inaccessible at this point.
  // Only s4 is live.

  let s5 = s4.clone(); // Deep copy
  println!("{}, {}", s4, s5); // No problem.

  // More complex examples of borrow checking with data structures.


  let mut v: Vec<i32> = vec![1, 2, 3];
  let n: &i32 = &v[1]; // A pointer to an element of a vec is like an iterator
  // v.push(5); <- invalid! cannot mutate v while any element is borrowed

  // Independent borrows on struct fields.
  struct Point { x: f32, y : f32 }
  let mut p = Point { x: 0.0, y: 0.0 };
  {
    let n: &f32 = &p.x;
    // This is actually ok! Can borrow x and y separately, as changing one
    // will never affect the other.
    let m: &mut f32 = &mut p.y;
    *m = 1.1;
  }

  // Different kinds of borrows in implementation.
  impl Point {
    // &self means p.print() immutably borrows p.
    fn print(&self) {
      println!("({}, {})", self.x, self.y);
    }

    // &mut self means p.incr_x() mutably borrows p.
    fn incr_x(&mut self) {
      self.x += 1.0;
    }

    // self means p.consume() moves p.
    fn consume(self) {}
  }

  p.print();
  p.incr_x();
  p.consume();
  // p.print(); <- invalid! p has moved

  // Borrowing inner fields of enums.
  enum List<T> { Nil, Cons(T, Box<List<T>>) }
  let mut l: List<i32> = List::Cons(3, Box::new(List::Nil));
  match &l {
    List::Cons(n, _) => {
      // *n = 3; <- invalid! cannot assign to immutable borrowed content
      println!("{}", *n);
    }
    List::Nil => {}
  }
}

Exercises, part 2

  1. Do these three short exercises from the rustlings list: one, two, three.

  2. Uncomment the mod part2 in src/main.rs. Then in src/part2.rs, your task is to fill out a simple Event interface with two functions: has_conflict that should take another event and returns if they occur on the same date, and update_event that takes an event and shifts up over by one day. See test_event for sample usage. You need to both fill out the argument/return types for these functions as well as the implementation.

  3. For your last and largest challenge, you will implement a trie. As a refresher, see the helpful diagram on Wikipedia. Tries are a tree-based index over a set of strings, where each node is a single character. In Rust, this is represented as a recursive struct:

    struct Trie {
      chr: char,
      has: bool,
      children: Vec<Trie>,
    }
    

    For example, the strings “a”, “cc”, and “ab” would become the trie:

    {'\0', false, [
      {'a', true, [{'b', true, []}]},
      {'c', false, [{'c', true, []}]}
    ]}
    

    Your task is to implement a build and contains function that construct and search the trie, respectively. Make sure you do this without ever cloning a string!