DEV Community

Cover image for Rust 3 - Generics, traits, functional constructs, data structures, files
Petr Janik
Petr Janik

Posted on • Edited on

Rust 3 - Generics, traits, functional constructs, data structures, files

Generics

To prevent duplication of code like this:

fn largest_i32(list: &[i32]) -> i32 {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> char {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}
Enter fullscreen mode Exit fullscreen mode

we can simplify the code using generics:

fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}
Enter fullscreen mode Exit fullscreen mode

Structures

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}
Enter fullscreen mode Exit fullscreen mode
struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}
Enter fullscreen mode Exit fullscreen mode

Method

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}
Enter fullscreen mode Exit fullscreen mode

Enum

enum Result<T, E> {
    Ok(T),
    Err(E),
}
Enter fullscreen mode Exit fullscreen mode

Traits

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, 
        self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Enter fullscreen mode Exit fullscreen mode

Default implementation

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}
Enter fullscreen mode Exit fullscreen mode

Trait as a parameter

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}
Enter fullscreen mode Exit fullscreen mode

Bound trait

pub fn notify<T: Summary>(item1: &T, item2: &T) { }
Enter fullscreen mode Exit fullscreen mode

Multiple traits

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { }

// or
fn some_function<T, U>(t: &T, u: &U) -> i32
    where
        T: Display + Clone,
        U: Clone + Debug,
{}
Enter fullscreen mode Exit fullscreen mode

Return trait-compliant structure from function

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}
Enter fullscreen mode Exit fullscreen mode

Iterators

Iterator is anything that implements an Iterator trait.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
Enter fullscreen mode Exit fullscreen mode

Iterate vector

fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {}", val);
    }

        // or 
        for val in v1 {
        println!("Got: {}", val);
    }
}
Enter fullscreen mode Exit fullscreen mode

Functional constructs

Map

let a = [1, 2, 3];

let mut iter = a.iter().map(|x| 2 * x);

assert_eq!(iter.next(), Some(2));
assert_eq!(iter.next(), Some(4));
assert_eq!(iter.next(), Some(6));
assert_eq!(iter.next(), None);
Enter fullscreen mode Exit fullscreen mode

Filter

let a = [1, 4, 2, 3];

let sum = a.iter()
    .cloned()
    .inspect(|x| println!("about to filter: {}", x))
    .filter(|x| x % 2 == 0)
    .inspect(|x| println!("made it through filter: {}", x))
Enter fullscreen mode Exit fullscreen mode

Enumerate

let a = ['a', 'b', 'c'];

let mut iter = a.iter().enumerate();

assert_eq!(iter.next(), Some((0, &'a')));
assert_eq!(iter.next(), Some((1, &'b')));
assert_eq!(iter.next(), Some((2, &'c')));
assert_eq!(iter.next(), None);
Enter fullscreen mode Exit fullscreen mode

Skip

let a = [1, 2, 3];

let mut iter = a.iter().skip(2);

assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), None);
Enter fullscreen mode Exit fullscreen mode

Take

let a = [1, 2, 3];

let mut iter = a.iter().take(2);

assert_eq!(iter.next(), Some(&1));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), None);
Enter fullscreen mode Exit fullscreen mode

Sum

let a = [1, 4, 2, 3];

let sum: i32 = a.iter()
    .cloned()
    .filter(|x| x % 2 == 0)
    .sum();

println!("{}", sum);
Enter fullscreen mode Exit fullscreen mode

Zip

let enumerate: Vec<_> = "foo".chars().enumerate().collect();

// lazy evaluation of (0..)
let zipper: Vec<_> = (0..).zip("foo".chars()).collect();

assert_eq!((0, 'f'), enumerate[0]);
assert_eq!((0, 'f'), zipper[0]);

assert_eq!((1, 'o'), enumerate[1]);
assert_eq!((1, 'o'), zipper[1]);

assert_eq!((2, 'o'), enumerate[2]);
assert_eq!((2, 'o'), zipper[2]);
Enter fullscreen mode Exit fullscreen mode

Fold

let a = [1, 2, 3];

// the sum of all elements of the array
let sum = a.iter().fold(0, |acc, x| acc + x);

assert_eq!(sum, 6);
Enter fullscreen mode Exit fullscreen mode

It is better to use for than iterators for side effects.

Data structures

Vec

when we want to:

  • push elements at the end
  • use it as a stack
  • have an array with dynamic size
  • have an array on heap
fn main() {
    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}
Enter fullscreen mode Exit fullscreen mode

VecDeque

when we want to:

  • have a vector that allows inserting elements at the beginning
  • use it as a queue
  • use it as a double-ended queue
use std::collections::VecDeque;

let mut buf = VecDeque::new();
buf.push_back(4);
buf.push_back(5);
buf.insert(0, 3);
if let Some(elem) = buf.get_mut(1) {
    *elem = 7;
}

assert_eq!(buf[0], 3);
assert_eq!(buf[1], 7);
assert_eq!(buf[2], 5);
Enter fullscreen mode Exit fullscreen mode

HashMap

when we want to:

  • map keys to values
  • have a cache
  • use it as a dictionary
fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

        // insert a value only if it doesn't already exist
    scores.entry(String::from("Blue")).or_insert(70);
    scores.entry(String::from("Red")).or_insert(80);

    println!("Blue score: {}", scores.get("Blue").unwrap());
    println!("Yellow score: {}", scores["Yellow"]);
    println!("Red score: {}", scores["Red"]);
}
Enter fullscreen mode Exit fullscreen mode

HashSet

when we want to:

  • store elements withou duplicates
use std::collections::HashSet;
// Type inference lets us omit an explicit type signature (which
// would be `HashSet<String>` in this example).
let mut books = HashSet::new();

// Add some books.
books.insert("A Dance With Dragons".to_string());
books.insert("To Kill a Mockingbird".to_string());
books.insert("The Odyssey".to_string());
books.insert("The Great Gatsby".to_string());

// Check for a specific one.
if !books.contains("The Winds of Winter") {
    println!("We have {} books, but The Winds of Winter ain't one.",
             books.len());
}

// Remove a book.
books.remove("The Odyssey");
Enter fullscreen mode Exit fullscreen mode

BTreeMap

when we want to:

  • have a map ordered by keys
  • get elements in range
  • get smallest or largest element fast
  • find keys that are greater or smaller that other
fn main() {
    use std::collections::BTreeMap;

    // type inference lets us omit an explicit type signature (which
    // would be `BTreeMap<&str, u8>` in this example).
    let mut player_stats = BTreeMap::new();

    fn random_stat_buff() -> u8 {
        // could actually return some random value here - let's just return
        // some fixed value for now
        println!("Some intensive computation.");
        42
    }

    // insert a key only if it doesn't already exist
    player_stats.entry("health").or_insert(100);
    // unnecessary intensive computation
    player_stats.entry("health").or_insert(random_stat_buff());
    // insert a key using a function that provides a new value only if it
    // doesn't already exist
    player_stats
        .entry("health")
        .or_insert_with(random_stat_buff);

    // update a key, guarding against the key possibly not being set
    let stat = player_stats.entry("attack").or_insert(100);
    *stat += random_stat_buff();
}
Enter fullscreen mode Exit fullscreen mode
use std::collections::BTreeMap;

let mut movie_reviews = BTreeMap::new();

// review some movies.
movie_reviews.insert("Office Space",       "Deals with real issues in the workplace.");
movie_reviews.insert("Pulp Fiction",       "Masterpiece.");
movie_reviews.insert("The Godfather",      "Very enjoyable.");
movie_reviews.insert("The Blues Brothers", "I liked it a lot.");

// check for a specific one.
if !movie_reviews.contains_key("Les Misérables") {
    println!("We've got {} reviews, but Les Misérables ain't one.",
             movie_reviews.len());
}

// oops, this review has a lot of spelling mistakes, let's delete it.
movie_reviews.remove("The Blues Brothers");

// look up the values associated with some keys.
let to_find = ["Up!", "Office Space"];
for movie in &to_find {
    match movie_reviews.get(movie) {
       Some(review) => println!("{}: {}", movie, review),
       None => println!("{} is unreviewed.", movie)
    }
}

// Look up the value for a key (will panic if the key is not found).
println!("Movie review: {}", movie_reviews["Office Space"]);

// iterate over everything.
for (movie, review) in &movie_reviews {
    println!("{}: \"{}\"", movie, review);
}
Enter fullscreen mode Exit fullscreen mode

BinaryHeap

when we want to:

  • save elements but we need to process only the largest or most important
  • have a priority queue
use std::collections::BinaryHeap;

// Type inference lets us omit an explicit type signature (which
// would be `BinaryHeap<i32>` in this example).
let mut heap = BinaryHeap::new();

// We can use peek to look at the next item in the heap. In this case,
// there's no items in there yet so we get None.
assert_eq!(heap.peek(), None);

// Let's add some scores...
heap.push(1);
heap.push(5);
heap.push(2);

// Now peek shows the most important item in the heap.
assert_eq!(heap.peek(), Some(&5));

// We can check the length of a heap.
assert_eq!(heap.len(), 3);

// We can iterate over the items in the heap, although they are returned in
// a random order.
for x in &heap {
    println!("{}", x);
}

// If we instead pop these scores, they should come back in order.
assert_eq!(heap.pop(), Some(5));
assert_eq!(heap.pop(), Some(2));
assert_eq!(heap.pop(), Some(1));
assert_eq!(heap.pop(), None);

// We can clear the heap of any remaining items.
heap.clear();

// The heap should now be empty.
assert!(heap.is_empty())
Enter fullscreen mode Exit fullscreen mode

Files

Read file to string

use std::env;
use std::fs;

fn main() {
    let filename = "foo.txt";
    println!("In file {}", filename);

    let contents = fs::read_to_string(filename)
        .expect("Something went wrong when reading the file");

    println!("With text:\n{}", contents);
}
Enter fullscreen mode Exit fullscreen mode

Working with large files

use std::fs::File;
use std::io::{self, prelude::*, BufReader};

fn main() -> io::Result<()> {
    let file = File::open("foo.txt")?;
    let reader = BufReader::new(file);

    for line in reader.lines() {
        println!("{}", line?);
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Write to file

use std::io::prelude::*;
use std::io::BufWriter;
use std::fs::File;

fn main() -> std::io::Result<()> {
    let mut buffer = BufWriter::new(File::create("foo.txt")?);

    buffer.write_all(b"some bytes")?;
    buffer.write_all("Hello World".as_bytes())?;
    buffer.flush()?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Append to file

use std::fs::OpenOptions;
use std::io::prelude::*;

fn main() {
    let mut file = OpenOptions::new()
        .write(true)  // before version 1.8, both write and append
        .append(true) // had to be true; now append(true) is enough
        .create(true)
        .open("foo.txt")
        .unwrap();

    if let Err(e) = writeln!(file, "A new line!") {
        eprintln!("Couldn't write to file: {}", e);
    }
}
Enter fullscreen mode Exit fullscreen mode

Exercises

Password converter

Task

Create a program that reads password entries from first file in one format and writes them to second file in CSV format.
When there are multiple entries with the same url and login, export the last one.

Solution

Password converter

Adventure game

Task

Create an adventure game. There will be a file with scenes. Each scene contains a story and choices the user can make. Each choice moves the user to another scene. Some scenes will be marked as end scenes.

Solution

Adventure game


Check out my learning Rust repo on GitHub!

Top comments (0)