Bitterless Rust

The Dynamic Ones

Oversimplification warning: This chapter is especially loose with the truth. What we cover here is an extreme simplification of core Rust concepts called "ownership," "move semantics," and "borrowing." For the real deal, see The Book, Chapter 4.

What Are "Dynamic Things"

Rust types come in 2 flavors (oversimplified):

Kind Examples Characteristics
Fixed-size things i32, f64, bool, char They copy automatically. No worries
Dynamic-size things String, Vec<T> Some special rules apply

i32 and f64 are always 4 bytes or 8 bytes -- fixed size. They get copied automatically when you assign or pass them:

fn main() {
    let a = 42;
    let b = a;     // copied
    println!("{}", a); // OK! a is still usable
}

String and Vec Are Special

String and Vec can change in length. They have special rules:

fn main() {
    let a = String::from("hello");
    let b = a;     // a's contents "move" to b
    // println!("{}", a); // Compile error! a is no longer usable
    println!("{}", b); // OK
}

Why? -- Don't worry about it. Just remember: "when you hand off a dynamic thing, it disappears from the original."

Same with function calls:

fn print_it(s: String) {
    println!("{}", s);
}

fn main() {
    let name = String::from("Alice");
    print_it(name);
    // println!("{}", name); // Error! name is gone
}

The Fix: clone

When in doubt, .clone() it. A copy is created so the original stays usable:

fn main() {
    let a = String::from("hello");
    let b = a.clone();    // make a copy
    println!("{}", a);    // OK!
    println!("{}", b);    // OK!
}
fn print_it(s: String) {
    println!("{}", s);
}

fn main() {
    let name = String::from("Alice");
    print_it(name.clone());   // pass a copy
    println!("{}", name);     // OK!
}

"Isn't .clone() a waste of memory?" -- Yes. But don't worry about it now. If it works, it works. Optimize when performance actually becomes a problem.

Vec: A Dynamic Array

Vec is an array that can change in length:

fn main() {
    // Various ways to create one
    let mut numbers: Vec<i32> = Vec::new();
    numbers.push(1);
    numbers.push(2);
    numbers.push(3);

    // One-liner with a macro
    let colors = vec!["red", "green", "blue"];

    println!("{:?}", numbers); // [1, 2, 3]
    println!("{:?}", colors);  // ["red", "green", "blue"]
}

Common Operations

fn main() {
    let mut v = vec![10, 20, 30, 40, 50];

    // Length
    println!("len: {}", v.len()); // 5

    // Access (index is usize)
    let i: usize = 0;
    println!("first: {}", v[i]); // 10

    // Add
    v.push(60);

    // Remove last
    v.pop();

    // Loop
    for item in v.clone() {
        println!("{}", item);
    }

    // v is still usable (because we cloned)
    println!("{:?}", v);
}

for item in v consumes v, making it unusable afterwards. Pass v.clone() instead.

Iterator Methods

Vec comes with methods you can use instead of loops. If you've used map or filter on arrays in other languages, these will feel familiar.

First, you need to know closures (anonymous functions):

|x| x + 1          // takes x, returns x + 1
|x, y| x + y       // two parameters
|x| { x * 2 }      // braces are fine too
|x| x % 2 == 0     // returning a bool works too

Oversimplification warning: Closures have an important property called "capturing the environment," but for now think of them as "short functions you can write inline."

map: Transform Each Element

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

    let doubled: Vec<i32> = numbers
        .iter()
        .map(|x| x * 2)
        .collect();

    println!("{:?}", doubled); // [2, 4, 6, 8, 10]
}

.iter() produces each element, .map() transforms them, .collect() gathers them back into a Vec. This three-step combo is the basic pattern.

.iter() creates an iterator that "just peeks at" elements. The original Vec isn't consumed, so no clone needed.

filter: Keep Only Matching Elements

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6];

    let evens: Vec<&i32> = numbers
        .iter()
        .filter(|x| *x % 2 == 0)
        .collect();

    println!("{:?}", evens); // [2, 4, 6]
}

filter's closure receives &&i32 (a reference to a reference). Write **x, or use |&&x| for pattern matching, or *x... honestly it's confusing. Just follow the compiler errors and add/remove * until it works.

Chaining map and filter

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Keep only evens, then double them
    let result: Vec<i32> = numbers
        .iter()
        .filter(|x| *x % 2 == 0)
        .map(|x| x * 2)
        .collect();

    println!("{:?}", result); // [4, 8, 12, 16, 20]
}

Chain them with method calls. Easy to read.

enumerate: Loop with Index

fn main() {
    let fruits = vec!["apple", "banana", "cherry"];

    for (i, fruit) in fruits.iter().enumerate() {
        println!("{}: {}", i, fruit);
    }
    // 0: apple
    // 1: banana
    // 2: cherry
}

enumerate() returns a tuple of (usize, &element).

find: First Matching Element

fn main() {
    let numbers = vec![1, 3, 5, 8, 10, 13];

    let first_even = numbers.iter().find(|x| *x % 2 == 0);

    match first_even {
        Some(n) => println!("found: {}", n), // found: 8
        None => println!("not found"),
    }
}

find returns an Option. If nothing matches, you get None.

any / all: Condition Checks

fn main() {
    let numbers = vec![2, 4, 6, 8];

    let has_odd = numbers.iter().any(|x| x % 2 != 0);
    println!("has odd: {}", has_odd); // false

    let all_positive = numbers.iter().all(|x| *x > 0);
    println!("all positive: {}", all_positive); // true
}

sum / count

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

    let total: i32 = numbers.iter().sum();
    println!("sum: {}", total); // 15

    let how_many = numbers.iter().filter(|x| *x > 3).count();
    println!("count > 3: {}", how_many); // 2
}

sum() often needs a type annotation (: i32). If the compiler says "I don't know what type to sum into," just add one.

Summary: Common Iterator Methods

Method What it does Returns
.iter() Creates an iterator (doesn't consume the original) Iterator
.map(|x| ...) Transforms each element Iterator
.filter(|x| ...) Keeps only matching elements Iterator
.collect() Converts an iterator into a Vec, etc. Vec, etc.
.enumerate() Attaches an index Iterator
.find(|x| ...) First matching element Option
.any(|x| ...) Whether any element matches bool
.all(|x| ...) Whether all elements match bool
.sum() Total Numeric type
.count() Count elements usize

You can do the same with for loops, but iterator methods make your intent clearer. They're used constantly in Rust, so get comfortable with them.

Vec and String Are Both "Dynamic Things"

fn main() {
    let a = vec![1, 2, 3];
    let b = a;
    // println!("{:?}", a); // Error!
    println!("{:?}", b);    // OK
}

Same rules as String. Clone to fix it.

HashMap: Key-Value Pairs

Bonus. The equivalent of dictionaries or maps in other languages:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Alice"), 100);
    scores.insert(String::from("Bob"), 85);

    // Retrieve
    match scores.get("Alice") {
        Some(score) => println!("Alice: {}", score),
        None => println!("not found"),
    }

    // Loop
    for (name, score) in scores.clone() {
        println!("{}: {}", name, score);
    }
}

Don't forget use std::collections::HashMap; at the top. get returns an Option -- we'll explain that in the next chapter.

Summary

  • i32, f64, bool -> They get copied. Don't worry
  • String, Vec, HashMap -> They disappear when handed off. When in doubt, .clone()
  • Don't overthink the deeper reasons for now

Next chapter, we'll learn Option and Result, and that'll wrap up the Rust fundamentals. Then you'll be ready to build a calculator.

Related

Back to book