Starting with Rust Idioms

Tomáš Jašek

March 7, 2023

Tomáš Jašek

Idiom

Wikipedia:

group of code fragments sharing an equivalent semantic role which recurs frequently across software projects

Why?

Groups of Idioms to be Discussed

  1. Functional approach with Option & Result
  2. Iterator
  3. Type Conversions

Part 1: Functional Approach with Option & Result

Part 1: Functional Approach with Option & Result

Introduction

enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Part 1: Functional Approach with Option & Result

Motivating Example

struct Request {
    parseable_user: Option<ParseableUser>,
}

struct ParseableUser { ... }

impl ParseableUser {
    fn parse(self) -> parse::Result<User> { ... }
}
struct User {
    display_name: Option<String>,
}

pub mod parse {
    pub struct Error;
    pub type Result<T> = Result<T, Error>;
}

Part 1: Functional Approach with Option & Result

Possible Solution

pub fn get_user_display_name(req: Request) -> Result<String> {
    if let Some(parseable_user) = req.parseable_user {
        if let Ok(user) = parseable_user.parse() {
            if let Some(display_name) = user.display_name {
                Ok(display_name)
            } else {
                Ok("".to_string())
            }
        } else {
            Err(parse::Error {})
        }
    } else {
        Ok("".to_string())
    }
}

Part 1: Functional Approach with Option & Result

Towards functional Approach

pub fn get_user_display_name(req: Request) -> Result<String> {
    if let Some(parseable_user) = req.parseable_user {
        if let Ok(user) = parseable_user.parse() {




            Ok(user.display_name.unwrap_or("".to_string()))
        } else {
            Err(parse::Error {})
        }
    } else {
        Ok("".to_string())
    }
}

Option::unwrap_or

Part 1: Functional Approach with Option & Result

“Practically readable” functional approach

pub fn get_user_display_name(req: Request) -> Result<String> {
    if let Some(parseable_user) = req.parseable_user {
        let user = parseable_user.parse()?;




        Ok(user.display_name.unwrap_or("".to_string()))



    } else {
        Ok("".to_string())
    }
}

question mark

Part 1: Functional Approach with Option & Result

Bonus: Fully functional approach

pub fn get_user_display_name(req: Request) -> Result<String> {
    Ok(req
        .parseable_user
        .map(|pu| pu.parse())
        .transpose()?
        .and_then(|u| u.display_name)
        .unwrap_or("".to_string()))
}

Important: does this reduce cognitive overhead for your team?

Part 2: Iterator

Part 2: Iterator

Introduction

Part 2: Iterator

Antipattern: iter -> collect -> iter -> collect

pub fn get_nonempty(vec: &Vec<String>) -> Vec<&String> {
    vec.iter().filter(|x| !x.is_empty()).collect()
}

fn transform_vec(vec: &Vec<String>) -> Vec<String> {
    get_nonempty(vec)
        .iter()
        .map(|x| format!("{} ", x))
        .collect()
}

let vec = vec![];
let vec = transform_vec(&vec);

Part 2: Iterator

Antipattern FIX: iter -> collect -> iter -> collect

pub fn get_nonempty(vec: &Vec<String>) -> impl Iterator<Item = &String> {
    vec.iter().filter(|x| !x.is_empty())
}

fn transform_vec(vec: &Vec<String>) -> Vec<String> {
    get_nonempty(vec)
        .map(|x| format!("{} ", x))
        .collect()
}

let vec = vec![];
let vec = transform_vec(&vec);

Part 2: Iterator

Example 1: Contest results

Prepare results sheet for a Contest. For each contestant, print their score and their name.

struct Contestant {
    name: String,
    score: usize,
}

struct Country {
    name: String,
    contestants: Vec<Contestant>,
}

struct Contest {
    countries: Vec<Country>,
}

Part 2: Iterator

Example 1: Contest results

Prepare results sheet for a Contest. For each contestant, print their score and their name.

fn results_list(contest: Contest) -> Vec<String> {
    let mut result = vec![];
    for country in &contest.countries {
        for contestant in &country.contestants {
            result.push(format!("{} | {}", contestant.score, contestant.name));
        }
    }
    result
}

Part 2: Iterator

Example 1: Contest results

Prepare results sheet for a Contest. For each contestant, print their score and their name.

fn results_list(contest: Contest) -> Vec<String> {
    let mut result = vec![];
    let contestants = contest.countries.iter().flat_map(|country| country.contestants);
    for contestant in contestants {
        result.push(format!("{} | {}", contestant.score, contestant.name));
    }
    result
}

Idiom: Iterator::flat_map

Notice: hybrid approach – prepare elements in functional way, iterate using for

Part 2: Iterator

Example 1: Contest results

Prepare results sheet for a Contest. For each contestant, print their score and their name.

fn results_list(contest: Contest) -> Vec<String> {
    contest.countries
        .iter()
        .flat_map(|country| country.contestants.iter())
        .map(|contestant| format!("{} | {}", &contestant.score, &contestant.name))
        .collect()
}

Idiom: Iterator::collect

Part 2: Iterator

Example 1: Contest results

Prepare results sheet for a Contest. For each contestant, print their score and their name.

fn results_list(contest: Contest) -> impl Iterator<Item = String> {
    contest.countries
        .iter()
        .flat_map(|country| country.contestants.iter())
        .map(|contestant| format!("{} | {}", &contestant.score, &contestant.name))
}

Idiom: return iterator

Part 2: Iterator

Example 2: Matrix transpose

Given a matrix A = (a_{i,j}), produce a transposed matrix. A^T = ( a_{j,i} ). Example:

\begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix} ^T = \begin{pmatrix} 1 & 4 \\ 2 & 5 \\ 3 & 6 \end{pmatrix}

struct Matrix {
    rows: Vec<Vec<isize>>,
    row_count: usize,
    col_count: usize,
}

Part 2: Iterator

Example 2: Matrix transpose

Given a matrix A = (a_{i,j}), produce a transposed matrix. A^T = ( a_{j,i} ).

impl Matrix {
    fn transpose(&self) -> Matrix {
        let mut result = Matrix::zero(self.col_count, self.row_count);

        for row in 0..self.row_count {
            for col in 0..self.col_count {
                result.set_element_at(col, row, self.element_at(row, col));
            }
        }

        result
    }
}

Part 2: Iterator

Example 2: Matrix transpose

Given a matrix A = (a_{i,j}), produce a transposed matrix. A^T = ( a_{j,i} ).

impl Matrix {
    fn transpose_iterators(&self) -> Matrix {
        let rows = (0..self.col_count)
            .map(|ncol| {
                self.rows
                    .iter()
                    .map(|row| row[ncol])
                    .collect::<Vec<isize>>()
            })
            .collect();
        Matrix {
            rows,
            row_count: self.col_count,
            col_count: self.row_count,
        }
    }
}

Pros: might be more performant

Cons: cognitive overhead, detailed documentation is required

Part 3: Type Conversions

Part 3: Type Conversions

From/Into: Infallible

Part 3: Type Conversions

From/Into: Infallible

We write

impl From<Wood> for Paper { ... }

Rust implements

impl Into<Paper> for Wood { ... }

Part 3: Type Conversions

From/Into: Infallible

We write

impl From<Wood> for Paper { ... }
impl Into<Paper> for Wood { ... }

Rust implements

impl Into<Paper> for Wood { ... }

N/A

Part 3: Type Conversions

From/Into: Infallible

Example: Convert between representations

struct Birthday(Date);
struct FullPerson {
    name: String,
    birthday: Birthday,
    birth_number: String,
    address: FullAddress,
}

struct FullAddress {
    street_number: String,
    street: String,
    city: String,
    country: String,
}
struct Age(Duration);
struct PartialPerson {
    name: String,
    age: Age,
    address: PartialAddress,
}

struct PartialAddress {
    city: String,
    country: String,
}

Part 3: Type Conversions

From/Into: Infallible

Example: Convert between representations

impl From<FullPerson> for PartialPerson {
    fn from(fp: FullPerson) -> Self {
        Self {
            name: fp.name,
            age: fp.birthday.into(),
            address: fp.address.into()
        }
    }
}

impl From<FullAddress> for PartialAddress {
    fn from(fa: FullAddress) -> Self {
        Self {
            city: fa.city,
            country: fa.country,
        }
    }
}
impl From<Birthday> for Age {
    fn from(bd: Birthday) -> Self {
        Self(SystemTime::now() - bd.0)
    }
}

Part 3: Type Conversions

TryFrom/TryInto: Fallible

Part 3: Type Conversions

TryFrom/TryInto: Fallible

Example: Users must be at least 13 years old to register.

struct User {
    name: String,
    age: usize,
}

Part 3: Type Conversions

TryFrom/TryInto: Fallible

Example: Users must be at least 13 years old to register.

struct User {
    name: String,
    age: usize,
}

struct UserIneligible;

fn register(u: User) -> Result<(), UserIneligible> {
    if u.age < 13 {
        return Err(UserIneligible);
    }

    // TODO: insert user into database

    Ok(())
}

Part 3: Type Conversions

TryFrom/TryInto: Fallible

Example: Users must be at least 13 years old to register.

struct User {
    name: String,
    age: usize,
}

struct UserIneligible;


fn register(u: User) -> Result<(), UserIneligible> {
    let u = EligibleUser::try_from(u)?;

    // TODO: insert user into database

    Ok(())
}
struct EligibleUser(User);

impl TryFrom<User> for EligibleUser {
    type Error = UserIneligible;

    fn try_from(u: User) -> Result<Self, Self::Error> {
        if u.age < 13 {
            return Err(UserIneligible);
        }

        Ok(Self(u))
    }
}

Part 3: Type Conversions

FromStr

Non-empty string:

struct NonEmptyString(String);

impl FromStr for NonEmptyString {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.empty() {
            return Err(());
        }

        Ok(Self(s.into()))
    }
}

assert_eq!("".parse::<NonEmptyString>(), Err(()));
assert_eq!("hello".parse::<NonEmptyString>(), Ok(NonEmptyString("hello".into())));

Part 3: Type Conversions

Useful external crates: Serde

#[derive(Serialize, Deserialize, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point = Point { x: 1, y: 2 };

    // Convert the Point to a JSON string.
    let serialized = serde_json::to_string(&point).unwrap();

    // Prints serialized = {"x":1,"y":2}
    println!("serialized = {}", serialized);

    // Convert the JSON string back to a Point.
    let deserialized: Point = serde_json::from_str(&serialized).unwrap();

    // Prints deserialized = Point { x: 1, y: 2 }
    println!("deserialized = {:?}", deserialized);
}

Credit: serde docs

Part 3: Type Conversions

Useful external crates: Serde

serde
serde

Part 3: Type Conversions

Useful external crates: Strum

#[derive(Debug, PartialEq, EnumString)]
enum Color {
    Red,
    Green {
        range: usize,
    },

    #[strum(serialize = "blue", serialize = "b")]
    Blue(usize),

    #[strum(disabled)]
    Yellow,

    #[strum(ascii_case_insensitive)]
    Black,
}
let color_variant = Color::from_str("Red").unwrap();
assert_eq!(Color::Red, color_variant);

Credit: strum docs

Summary