Skip to content

Instantly share code, notes, and snippets.

@bsodmike
Last active May 22, 2020 00:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bsodmike/8d60c8aa8f47ca2735dab2a167b8c06a to your computer and use it in GitHub Desktop.
Save bsodmike/8d60c8aa8f47ca2735dab2a167b8c06a to your computer and use it in GitHub Desktop.
Rust resources and code snippets to improve writing idiomatic Rust.

Why Rust?

  • Memory safe. I like to think of it as being given a dull-rusty blade rather than a sharp finely crafted japanese katana/wakizashi. Now go run about and see which cuts you faster!
  • Support for Futures (aka Promises in JS parlance).
  • Threadsafe concurrency features: Arc vs. Mutex<T> etc.
  • Generics and Zero-cost abstractions
  • Runs on anything with a CPU. Even Android.
  • Supports full WASM (WebAssembly). Here's a demo.
  • Fabulous tooling via Cargo.
  • Much more covered by this great talk by Jon Gjengset (@jonhoo) https://www.youtube.com/watch?v=DnT-LUQgc7s

Strings

Due to the way borrowing works, this is possible

// Easily convert to &str, due to dereferencing
let s = String::new();
// Taking a slice is also valid but more verbose
let t: &str = &s[..];
// This is cleaner.
let u: &str = &s;

let s = String::new();
let t = String::new();
let combined = s + &t;

Here's an example to look at Unicode encoding of strings. Rust by default encodes Strings (and string slices) as UTF-8.

let s = String::from("Hey,");
let t = String::from(" oh hai");
let combined = s + &t;

for char in combined.chars() {
    let value: String = char.escape_unicode().collect();
    println!("Char {} to hex Unicode escape {}", char, value);
}

// Output:
//  
// Char H to hex Unicode escape \u{48}
// Char e to hex Unicode escape \u{65}
// Char y to hex Unicode escape \u{79}
// Char , to hex Unicode escape \u{2c}
// Char   to hex Unicode escape \u{20}
// Char o to hex Unicode escape \u{6f}
// Char h to hex Unicode escape \u{68}
// Char   to hex Unicode escape \u{20}
// Char h to hex Unicode escape \u{68}
// Char a to hex Unicode escape \u{61}
// Char i to hex Unicode escape \u{69}

let char = "\u{63}".chars().next();
if let Some(i) = char {
    let mut b = [0; 1];
    let value = i.encode_utf8(&mut b);
    println!("Char {}", value);
}

// Output:
//  
// Char c

// Similarly we can see that a String containing this particular emoji has a capacity, which is the size of the content allocated on the heap, as the Unicode scalar value takes 4-bytes of storage.
let a = String::from("🐶");
println!("Output {}", a.capacity());
// Output 4
let b = String::from("a");
println!("Output {}", b.capacity());
// Output 1

Regex

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=1c8ea00f570e798fa40bd297e5d7e07b

Unit structs

  struct Handler;
  trait EventHandler {
    fn foo(&self) -> &str;
  }
  impl EventHandler for Handler {
    fn foo(&self) -> &'static str {
      let message = "hello";
      println!("Handler: {}", message);

      message
    }
  }

  let myHandler = Handler;
  myHandler.foo();

Traits

use std::fmt;
use std::panic;

#[derive(Debug)]
#[allow(unused_imports)]
struct MyStrangeStruct<T> {
    items: Vec<T>,
    age: u8,
}

fn main() {
    let my_s = MyStrangeStruct {
        age: 20,
        items: vec![String::from("hello")],
    };
    println!("{:#?}", my_s);
}

In the previous example we could have defined the Debug trait ourselves if needed. This is useful if you are dealing with a type that does not already implement this trait. A common occurance is the need to implement the trait Display especially when printing them out to STDOUT.`

#[allow(unused_imports)]
struct MyStrangeStruct<T> {
    items: Vec<T>,
    age: u8,
}

impl<T> fmt::Debug for MyStrangeStruct<T>
where
    T: fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("MyStrangeStruct<T>")
            .field("age", &self.age)
            .field("items", &self.items)
            .finish()
    }
}

Bounded Parametric Polymorphism - Using Trait Objects

In this example, the goal is to be able to maintain a collection containing elements of different types.

use std::clone;
use std::default::Default;
use std::error;
use std::fmt;
use std::marker::Copy;

#[derive(Debug, Copy, Clone)]
pub enum State {
    Up,
    Down,
    Unknown,
}

#[derive(Debug, Copy, Clone)]
pub struct PollHTTPBodyContent;

#[derive(Debug, Copy, Clone)]
pub struct PollHTTPStatusOk;

pub trait Monitorable {
    fn info(&self) -> String;
    fn poll(&self);
}

pub struct Monitor<T> {
    context: T,
    state: State,
}

pub struct Monitored {
    pub enabled: Vec<Box<dyn Monitorable>>,
}

impl Default for State {
    fn default() -> Self {
        State::Unknown
    }
}

impl Default for Monitored {
    fn default() -> Monitored {
        Monitored {
            enabled: Vec::new(),
        }
    }
}

impl fmt::Debug for Monitored {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_list().entries(&self.enabled).finish()
    }
}

impl fmt::Debug for dyn Monitorable {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("Monitorable")
            .field("info", &self.info())
            .finish()
    }
}

impl Monitored {
    pub async fn new() -> Result<Monitored, Box<dyn error::Error>> {
        Ok(Monitored::default())
    }

    pub async fn add<T: 'static + Monitorable>(
        &mut self,
        item: Box<T>,
    ) -> Result<(), Box<dyn error::Error>> {
        self.enabled.push(item);

        Ok(())
    }
}

// PollHTTPBodyContent
impl fmt::Debug for Monitor<PollHTTPBodyContent> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("Monitor<PollHTTPBodyContent>")
            .field("state", &self.state)
            .field("context", &self.context)
            .finish()
    }
}

impl Default for Monitor<PollHTTPBodyContent>
where
    State: Default,
{
    fn default() -> Monitor<PollHTTPBodyContent> {
        Monitor::<PollHTTPBodyContent> {
            context: PollHTTPBodyContent {},
            state: State::default(),
        }
    }
}

impl Copy for Monitor<PollHTTPBodyContent> {}

impl clone::Clone for Monitor<PollHTTPBodyContent> {
    fn clone(&self) -> Self {
        *self
    }
}

impl Monitorable for Monitor<PollHTTPBodyContent> {
    fn info(&self) -> String {
        String::from("Monitor<PollHTTPBodyContent>")
    }
    fn poll(&self) {
        println!("poll() for {:#?}", self);
    }
}

impl Monitor<PollHTTPBodyContent> {
    pub async fn new() -> Result<Monitor<PollHTTPBodyContent>, Box<dyn error::Error>> {
        let monitor: Monitor<PollHTTPBodyContent> = Monitor::<PollHTTPBodyContent>::default();
        Ok(monitor)
    }
}

// PollHTTPStatusOk
impl fmt::Debug for Monitor<PollHTTPStatusOk> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("Monitor<PollHTTPStatusOk>")
            .field("state", &self.state)
            .field("context", &self.context)
            .finish()
    }
}

impl Default for Monitor<PollHTTPStatusOk>
where
    State: Default,
{
    fn default() -> Monitor<PollHTTPStatusOk> {
        Monitor::<PollHTTPStatusOk> {
            context: PollHTTPStatusOk {},
            state: State::default(),
        }
    }
}

impl Copy for Monitor<PollHTTPStatusOk> {}

impl clone::Clone for Monitor<PollHTTPStatusOk> {
    fn clone(&self) -> Self {
        *self
    }
}

impl Monitorable for Monitor<PollHTTPStatusOk> {
    fn info(&self) -> String {
        String::from("Monitor<PollHTTPStatusOk>")
    }
    fn poll(&self) {
        println!("poll() for {:#?}", self);
    }
}

The above allows adding separate Monitor types to the Vector accumulator in Monitored

pub async fn run() -> Result<(), Box<dyn error::Error>> {
    let mut monitored = Monitored::new().await?;
    let new_monitor: Monitor<PollHTTPBodyContent> = Monitor::<PollHTTPBodyContent>::new().await?;
    let new_monitor2: Monitor<PollHTTPStatusOk> = Monitor::<PollHTTPStatusOk>::new().await?;
    monitored.add(Box::new(new_monitor)).await?;
    monitored.add(Box::new(new_monitor2)).await?;

    println!("Monitored: {:#?}", &monitored);
    println!("Enabled monitor count: {}", &monitored.enabled.len());

    for item in monitored.enabled.iter() {
        item.poll();
    }

    Ok(())
}

// Monitored: [
//     Monitorable {
//         info: "Monitor<PollHTTPBodyContent>",
//     },
//     Monitorable {
//         info: "Monitor<PollHTTPStatusOk>",
//     },
// ]
// Enabled monitor count: 2
// poll() for Monitor<PollHTTPBodyContent> {
//     state: Unknown,
//     context: PollHTTPBodyContent,
// }
// poll() for Monitor<PollHTTPStatusOk> {
//     state: Unknown,
//     context: PollHTTPStatusOk,
// }

References and borrowing

https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html

  • & is immutable, &mut is mutable. Mutable refs (references) cannot coexist with other refs thus, & can also be referred to as shared or shared references, while &mut is exclusive. This is sort of core to what makes Rust different from most languages.
  • T is called a generic type parameter.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=97047bb3438368b5b9200db51348b79e

use std::fmt;
use std::ops;

#[derive(Debug)]
#[allow(unused_imports)]
struct MyStrangeStruct<T> {
    items: Vec<T>,
    age: u8,
}

fn main() {
    let mut input_a = 4.2;
    let a: f64 = my_func_mut(&mut input_a, &mut 5.);
    println!("a has {:.2}", a);

    let b: f64 = my_func_as_ref(&2.1, &12.7);
    println!("b has {:.2}", b);
}

fn my_func_mut<T>(input_a: &mut T, input_b: &mut T) -> T
where
    T: fmt::Debug + ops::Add<Output = T> + Copy + From<f64>,
{
    // Example of modifying contents of a mutable reference.
    let inc_val = 0.6;
    *input_a = *input_a + T::from(inc_val);
    // `T::from` above works only because this trait has been defined on the `generic type parameter` `T`.

    println!("my_func_mut: input_a has {:#?}", input_a);
    *input_a + *input_b
}

fn my_func_as_ref<T>(input_a: &T, input_b: &T) -> T
where
    T: fmt::Debug
        + ops::Add<Output = T>
        + ops::Sub<Output = T>
        + ops::Mul<Output = T>
        + ops::Div<Output = T>
        + Copy,
{
    println!("my_func_as_ref: input_a has {:#?}", input_a);
    *input_a / *input_b
}

Newtype pattern

use std::fmt;

#[derive(Debug)]
struct WrappedString(std::string::String);

impl fmt::Pointer for WrappedString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let ptr = self as *const Self;
        fmt::Pointer::fmt(&ptr, f)
    }
}

impl fmt::Display for WrappedString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Tip: the use of `self.0` below allows use to access the inner `String`
        // as WrappedString acts as a wrapper around `String`. This is an example
        // of using the Newtype pattern.
        // Bonus: had we just called `self`, notice that we cause recursion.
        // Use of the newtype prevents recursion as Display is called by the
        // inner `String`, rather than by `WrappedString`.
        //
        // References:
        // - https://doc.rust-lang.org/stable/book/ch19-03-advanced-traits.html#using-the-newtype-pattern-to-implement-external-traits-on-external-types
        // - https://doc.rust-lang.org/stable/book/ch19-04-advanced-types.html?highlight=newtype#using-the-newtype-pattern-for-type-safety-and-abstraction
        write!(f, "{}", self.0)
    }
}

impl std::ops::Add<&str> for WrappedString {
    type Output = WrappedString;

    fn add(self, other: &str) -> WrappedString {
        let mut ws = self;
        ws.0.push_str(other);
        ws
    }
}

impl WrappedString {
    fn new(s: String) -> WrappedString {
        WrappedString(s)
    }
}

fn main() {
    // `add()` is called at: https://github.com/rust-lang/rust/blob/master/src/liballoc/string.rs#L1996
    // this calls `push_str` at https://github.com/rust-lang/rust/blob/master/src/liballoc/string.rs#L836  
    //
    // Ref: https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#ways-variables-and-data-interact-move
    // Demonstrating images 4-3 and 4-4:
    // s1 got moved into add(), mutated, and then returned and you assigned it
    // to combined, so conceptually it's the same String, but it's not at the
    // same location.  This is due to the move by assigning it to a new variable
    // triggers the extra stack allocation.

    let s1 = WrappedString::new("Howdy".to_string());
    let s2 = String::from(" fella Rustacean!");
    println!("s1 address {}", format!("{:p}", &s1));
    println!("s2 address {}", format!("{:p}", &s2));

    let combined = s1 + &s2;

    println!("combined address {}", format!("{:p}", combined));
    println!("combined result: {}", combined);
}

Threads

Advanced scenario

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=358e8489b7ea5ffa14535b85616c7182

Example:

https://gist.github.com/bsodmike/a41f55e067292fd32960a9861b19e135

Panic

  • Example of adding custom logic around the default panic handler (Link). This is useful to send panic reports via email before the executable terminates.

Serde specific

  • Example of nesting serde_json::Value within a HashMap (Link)

Profiling performance

Learning resources

Acknowledgements

This gist has been compiled based on my learning from picking up Rust in just a couple days, which would have not been any easier had it not been for the monumental support found over at ##rust on Freenode IRC. This is a big thank you to all their continued support.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment