Rust Basics

The syntax of Rust resembles other Turing-family languages, like C++. We provide a list of the most useful Rust constructs for HW5, and their Python equivalents (where possible). The format is:

rust code
=====
python equivalent

Function calls

In Rust, you can call functions as you do in Python:

f(x,y)
g()
=====
f(x,y)
g()

Primitive types

Rust has the following primitive types:

Local variables

In Rust, you can define local variables using let:

let x = e1;
let y = e2;
f(x,y);
let z = e3;
g(z);
=====
x = e1
y = e2
f(x,y)
z = e3
g(z)

By default, local variables are immutable. To define a mutable local variable, use let mut:

let x = 1;
// x += 1; // compile error, x is immutable
let mut y = 1;
y += 1; // ok, y is now 2

The Rust compiler is generally very good at inferring types, but local variables support type annotations in case it needs help:

let x: i32 = 1;     // x is a 32-bit signed integer
let y: bool = true; // y is a boolean

Conditionals and loops

if e1 {
  e2
} else {
  e3
}

for (i, c) in vec![(1, 'a'), (2, 'b'), (3, 'c')].iter() {
  println!("i={}, c={}", i, c);
}
=====
if e1:
  e2
else:
  e3

for i, c in [(1, 'a'), (2, 'b'), (3, 'c')]:
  print(f"i={i}, c={c}")

Enum types and pattern matching

In Rust, you can define algebraic data types (ADTs) using enum and destruct them using match. For instance, the ADT type Option = None | Some i32 is:

enum Option {
  None,
  Some(i32),
}

let x = Some(3);
match x {
  Some(i) => println!("Some({})", i),
  None => println!("None"),
}

Defining functions

In Rust, you must explicitly write the type of functions' arguments and the return value:

pub fn foo(x: u32, y: u32) -> bool {
  println!("x={}, y={}", x, y);
  x == y
}
pub fn bar(z: char) {
  println!("z={}", z);
}
=====
def foo(x, y):
  print("foo: x={}, y={}".format(x, y))
  return x == y
def bar(z):
  print(z)

Note that, in Rust, the last expression in a function definition becomes the return value of the function. In general, Rust is "expression oriented" and the last expression in a block (expressed by {...}), if, etc. becomes the result:

let x = {
  let i = 1;
  let j = 2;
  i + j
};

let x = if e1 {
  e2
} else {
  e3
};

Functions can be generic over types and lifetimes:

pub fn foo<T>(x: T)         {} // takes a generic type T
pub fn bar<'a>(x: &'a i32)  {} // takes a borrow of an i32 with lifetime 'a
pub fn baz<'a, T>(x: &'a T) {} // takes a borrow of a T with lifetime 'a

Ownership and borrowing

Rust's ownership model is complex, and we strongly recommend reading or skimming through the following sections in the Rust book:

Defining structs and methods

In Rust, you can define a class using struct and define its methods using impl:

pub struct Bar { x: u32, y: Vec<char> }
impl Bar {
  pub fn new() -> Bar {
    Bar { x: 0, y: vec![] }
  }
  pub fn f(self) -> u32 { self.x }
  pub fn g(&self) -> u32 { self.x }
  pub fn h(&mut self, y_new: char) { self.y.push(y_new); }
}

let a: Bar = Bar::new();
a.f();
// a.f();  // compile error, as the previous line took the ownership of `a`.
let mut b: Bar = Bar::new();
b.g();
b.g();     // no compile error, as the previous line only borrowed the ownership of `b`.
b.h('c');
=====
class Bar:
  def __init__(self): self.x = 0; self.y = []
  def f(self): return self.x
  def g(self): return self.x
  def h(self, y_new): self.y.append(y_new)
  
a = Bar()
a.f()
a.f()  # no error in Python, as Python does not have the concept of ownership.
b = Bar()
b.g()
b.g()
b.h('c')

Like functions, structs can be generic over types and lifetimes:

pub struct Bar<'a, T> { x: T, y: &'a T }

Traits

Traits are the way to do inheritance of functionality in Rust. Traits declare abstract inferfaces which types can implement. Here is a variant of an example from "Rust by Example":

struct Sheep { name: &'static str }
trait Animal {
  fn name(&self) -> &'static str;
}
impl Animal for Sheep {
  fn name(&self) -> &'static str { self.name }
}

// This function takes any Animal and calls its name method.
fn print_name<T: Animal>(animal: T) {
  println!("Name: {}", animal.name());
}

let dolly = Sheep { name: "Dolly" };
print_name(dolly); // Sheep implements Animal, so we can pass a Sheep into print_name.

However, traits are more general than traditional single inheritance in OO languages:

trait Foo {
  fn foo(&self) -> u32;
}

// Implement Foo for any type that implements Animal.
impl<T: Animal> Foo for T {
  fn foo(&self) -> u32 { 42 }
}

let dolly = Sheep { name: "Dolly" };
dolly.foo(); // Sheep implements Animal, so Sheep implements Foo. This prints 42.

Like functions and structs, traits can be generic over types and lifetimes:

trait Foo<'a, T> {
  fn foo<'b, U>(&self, x: &'b U, y: &'a T) -> u32;
}

impl<'a> Foo<'a, u32> for Sheep {
  fn foo<'b, U>(&self, x: &'b U, y: &'a u32) -> u32 { 42 }
}

impl<'a> Foo<'a, bool> for Sheep {
  fn foo<'b, U>(&self, x: &'b U, y: &'a bool) -> u32 { 17 }
}

let dolly = Sheep { name: "Dolly" };
dolly.foo(&3, &4);       // Ok, Sheep implements Foo<u32>. This prints 42.
dolly.foo(&3, &true);    // Ok, Sheep implements Foo<bool>. This prints 17.
dooly.foo(&true, &true); // Ok, the first parameter of foo is generic
// dolly.foo(&3, &'c');  // Compile error: Sheep does not implement Foo<char>

Associated types are another way of adding generic parameters to traits:

trait Foo {
  type T;
  fn foo(&self) -> Self::T;
}

// Ok
impl<'a> Foo for Sheep {
  type T = u32;
  fn foo<'b, U>(&self, x: &'b U, y: &'a u32) -> u32 { 42 }
}

// // Compile error: since T is an associated type, we can
// // implement Foo for Sheep for ONLY ONE type T.
// impl<'a> Foo for Sheep {
//   type T = bool;
//   fn foo<'b, U>(&self, x: &'b U, y: &'a bool) -> u32 { 17 }
// }

Closures

In Rust, a closure (lambda) can be written:
let y = 1;
let f = |x: u32| -> u32 { x + y }; // with explicit argument and return types
let g = |x| { x + y };             // with implicit argument and return types
=====
y = 1
f = lambda x: x + y
g = lambda x: x + y
Since Rust has ownership and borrowing, there are three type of closures. Conceptually:
trait FnOnce<T> {
  type Output;
  fn call(self, args: T) -> Self::Output;      // takes its environment by move
}
trait FnMut<T> {
  type Output;
  fn call(&mut self, args: T) -> Self::Output; // takes its environment by mut borrow
}
trait Fn<T> {
  type Output;
  fn call(&self, args: T) -> Self::Output;     // takes its environment by borrow
}
Rust implements FnOnce, FnMut, and Fn automatically for the appropriate closures. These traits have their own special syntax, e.g., a closure that implements Fn(i32) -> bool takes an integer and returns a boolean.

Important data types