Nuggets of Rust


By Jitesh Doshi
On 2023-01-15T00:00:00.000Z

Ferris - Rust Mascot

I learned Rust long time back in a haphazard manner, and then I keep getting out of touch when I haven’t had a chance to use it continuously. So here I have put together a quick cheat sheet of things to remember.

It might help someone either refresh their Rust knowledge, or learn the basics for the first time.

Special thanks to https://fasterthanli.me/articles/a-half-hour-to-learn-rust for putting together such a nice concise list. Many of the nuggets below are adapted from there.

Rust Logo

Resources

Videos

Channels

Nuggets

Nuggets (tidbits) of useful knowledge gathered.

  • create project with cargo new myproject
myproject/
├── Cargo.toml
└── src
    └── main.rs
  • Two types of strings
    • String owned, growable string (is it always on the heap?)
    • &str borrowed primitive string slice ref
    • There is a Deref trait that allows String refs to be borrwed into &str
let s1: String = String::from("hi");
let s2: &str = &s1;
#[cfg(test)]
mod tests {
  #[test]
  fn mytestfn() {
    let result = ...;
    assert_eq!(result, expected)
  }
}
  • There’s no ternary operator. Use if/else with tail expressions
let x = if(age >= 18) { "adult" } else { "minor" }

Or a match

match {
  age < 18 => "minor",
  age < 65 => "adult",
  age >= 65 => "senior",
}
  • Units of code
    • crates are like libraries (modules in Go)
    • modules are like source files/file-groups (packages in Go)
  • use statement brings stuff into scope
use std::cmp::min;

let least = min(7, 1); // this is 1
  • curly braces can contain “glob patterns”
use std::cmp::{min, max};
use std::cmp::*; // bring everything into scope
  • methods that take self can be called as methods or functions
let l1 = "foo".len();
let l2 = str::len("foo");
  • Rust implicitly includes the following at the start of every module, which brings Vec, String, Option, Result, etc. into scope.
use std::prelude::v1::*;
  • Structs …
struct Vec2 {
    x: f64, // 64-bit floating point, aka "double precision"
    y: f64,
}
let v1 = Vec2 { x: 1.0, y: 3.0 };
let v2 = Vec2 { y: 2.0, x: 4.0 };
let v3 = Vec2 {
    x: 14.0,
    ..v2 // spread .. must be last, and does not override previous fields
};
let v4 = Vec2 { ..v3 }; // spread .. all fields
let Vec2 {x, y} = v2; // destructuring: x = 2.0, y = 4.0
let Vec2 {x, ..} = v2; // destructuring: discard y
let v5 = Vec2{x, y:6} // same as Vec2{x:x, y:6}
  • Methods … can be implemented only on your types
impl Vec2 {
    fn diag(&self) -> f64 {
      f64::sqrt(self.x * self.x + self.y * self.y)
    }
}
  • Traits … can be implemented on anyone’s types (as long as the trait or the type is yours)
trait Signed {
    fn is_strictly_negative(self) -> bool;
}
// ok, even if Number is not your type
impl Signed for Number {
    // self if of type that is the target of impl-for
    fn is_strictly_negative(self) -> bool {
        self.value < 0
    }
}
// even a primitive type
impl Signed for i32 {
    fn is_strictly_negative(self) -> bool {
        self < 0
    }
}
  • marker traits (e.g. Copy) have no methods, but they indicate that a type has properties (e.g. copy-itself).
let a:i32 = 5;
let b = a; // copied!
let c: Number = ...;
let d = c; // moved!
  • Some traits can be implemented with #[derive] attribute.
#[derive(Clone, Copy)]
struct Number {
    odd: bool,
    value: i32,
}
  • Generics …
// trait Display is a constraint of T
fn print<T: Display>(value: T) {
    println!("value = {}", value);
}

// trait Debug is a constraint of T
fn print<T: Debug>(value: T) {
    println!("value = {:?}", value);
}

fn print<T>(value: T)
where
  T: Display + Debug, // multiple constraints
{
  // ...
}
  • compile time reflection? (::<> is called turbofish syntax)
fn main() {
    use std::any::type_name;
    println!("{}", type_name::<i32>()); // prints "i32"
    println!("{}", type_name::<(f64, char)>()); // prints "(f64, char)"
}
  • Option is an enum, with two variants. So is Result. Both can be unwrapd. But better yet, use expect to fail with a custom message.
enum Option<T> {
    None,
    Some(T),
}
fn main() {
    let o1: Option<i32> = Some(128);
    o1.unwrap(); // this is fine

    let o2: Option<i32> = None;
    o2.unwrap(); // this panics!
}
enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • Function reference parameter lifetimes … Named lifetimes allow returning references whose lifetime depend on the lifetime of the arguments. There is a special lifetime, named 'static, which is valid for the entire program’s lifetime.
// elided (non-named) lifetimes:
fn print(x: &i32) {}

// named lifetimes:
fn print<'a>(x: &'a i32) -> &'a i32{}
// not really needed in this case, since this is default.
  • Structs can also be generic over lifetimes, which allows them to hold references
struct NumRef<'a> {
    x: &'a i32,
}

fn main() {
    let x: i32 = 99;
    let x_ref = NumRef { x: &x };
    // `x_ref` cannot outlive `x`, etc.
}
  • For many types in Rust, there are owned and non-owned variants:
    • Strings: String is owned, &str is a reference
    • Paths: PathBuf is owned, &Path is a reference
    • Collections: Vec<T> is owned, &[T] is a reference

Pearls

Pearls (real gems) of wisdom gathered.

  • If you want to do some complex computation and use the result, you don’t have to write a function or IIFE, etc. Just use the “tail” of a block.
let x = {
  let y = 1; // first statement
  let z = 2; // second statement
  y + z // this is the *tail* - what the whole block will evaluate to
};
  • let (destructuring) patterns can be used as conditions in if. And so can match arms (though a match has to be exhaustive; use _ as the catch-all arm):
if let Number { odd: true, value } = n {
    println!("Odd number: {}", value);
} else if let Number { odd: false, value } = n {
    println!("Even number: {}", value);
}

match n {
  Number { odd: true, value } => println!("Odd number: {}", value),
  Number { odd: false, value } => println!("Even number: {}", value),
}
  • The generic part of a type can be inferred from usage …
fn main() {
    let mut v1 = Vec::new();
    v1.push(1); // v1 is now Vec<i32>
    let mut v2 = Vec::new();
    v2.push(false); // v2 is now Vec<bool>
}
  • While borrowed, a variable binding cannot be mutated:
fn main() {
    let mut x = 42;
    let x_ref = &x;
    x = 13;
    println!("x_ref = {}", x_ref);
    // error: cannot assign to `x` because it is borrowed
}
  • While immutably borrowed, a variable cannot be mutably borrowed:
fn main() {
    let mut x = 42;
    let x_ref1 = &x;
    let x_ref2 = &mut x;
    // error: cannot borrow `x` as mutable because it is also borrowed as immutable
    println!("x_ref1 = {}", x_ref1);
}
  • Use ? to forward error …
fn foo() -> Result<i32, std::str::Utf8Error> {
  let x = some_fn_that_could_err()?;
  Ok(x)
}