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
In Rust, you can call functions as you do in Python:
f(x,y)
g()
=====
f(x,y)
g()
i8, i16, i32, i64, i128 and isize (pointer size)u8, u16, u32, u64, u128 and usize (pointer
size)f32, f64char, a unicode character like 'a' or '🦀'bool, either true or false(1, true)(), whose only possible value is an empty tuple: ()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
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}")
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"),
}
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
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 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 }
// }
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.
Vec is Rust's array/list type. Some important operations on vectors:
let u: Vec<u32> = vec![1,3,5];
let mut v: Vec<u32> = u.clone(); // copy a vector
v.push(7); // append 7 to v
println!("v={:?}, len={}", v, v.len()); // print v and its length
for v_val in v.iter() { // iterate over v
println!("{}", *v_val); // print each element of v
}
=====
u = [1,3,5]
v = list(u)
v.append(7)
print("v={}, len={}".format(v, len(v)))
for v_val in v:
print(v_val)
Option is the ADT type Option T = None | Some(T). This is Rust's name for the Maybe monad we defined in lecture.
let x: Option<u32> = None;
let y: Option<u32> = Some(3);
match y {
Some(i) => println!("Some({})", i),
None => println!("None"),
}
let z = y.map(|i| i == 3); // y was Some(3), so z is Some(true)
// if y were Some(4), z would be Some(false)
// if y were None, z would be None
let w = y.unwrap(); // y was Some(3), so w is 3
// if y were None, this would panic (terminate the program)
Result is the ADT type Result T E = Err(E) | Ok(T). This Rust monad is similar to Maybe/Option, except it carries extra information in the error case.
let x: Result<u32, &str> = Err("error");
match x {
Ok(i) => println!("Ok({})", i),
Err(s) => println!("Err({})", s),
}
There is special syntax in Rust for propagating errors using Result:
fn f() -> Result<u32, &str> {
let x: Result<u32, &str> = g();
let y = x?; // if x is Ok(i), then y is i; if x is Err(s), then return Err(s)
// do something with y
}
// The above is equivalent to:
fn f() -> Result<u32, &str> {
let x: Result<u32, &str> = g();
let y = match x {
Ok(i) => i,
Err(s) => return Err(s),
};
// do something with y
}