Study Topic: Language: Rust

From Matt Morris Wiki
Jump to navigation Jump to search

This is a Study Leave topic.

Language Notes

Samples

temp

use std::io;

fn main() {
    let mut input = String::new();

    io::stdin().read_line(&mut input)
        .expect("Failed to read line");
        
    let input = "-272C";
    
    let scale = input.get(input.len()-1..).unwrap_or("F");
    
    println!("Last {}", scale);

    let input: f32 = input.get(..input.len()-1).unwrap_or("0").parse()
        .expect("Please type a number!");

    println!("Input {}", input);
    
    if scale == "F" {
        let trans = (input - 32.0) * 5.0 / 9.0;
        println!("Answer {0}F = {1}C", input, trans);
    }
    else if scale == "C" {
        let trans = (input * 9.0 / 5.0) + 32.0;
        println!("Answer {0}C = {1}F", input, trans);
    }
    else {
        panic!("Unknown scale: '{}'", scale);
    }
}​

fib

use std::io;

fn main() {
    let mut input = String::new();

    io::stdin().read_line(&mut input)
        .expect("Failed to read line");
        
    let input = "30";
    
    let input: u64 = input.parse()
        .expect("Please type a number!");

    println!("Input {}", input);
    
    let output = calc_fib(input);
    println!("Output {}", output);
    
    let output2 = calc_fib2(input);
    println!("Output2 {}", output2);
}

fn calc_fib(n : u64) -> u64  {
    if n == 0 || n == 1 {
        return 1;
    }
    return calc_fib(n-1) + calc_fib(n-2);
}

fn calc_fib2(n : u64) -> u64  {
    let mut f = (1, 1);
    for _ in 1..n {
        f = (f.1, f.0 + f.1);
    }
    return f.1;
}​

Ownership

  • No ref => transfers ownership
  • & => immutable ref
  • &mut => mutable ref

Can have n-immutable OR 1-mutable,but not both

Structs

definition - with debug output support

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

memfunc with no parameters

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

memfunc with parameter

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}​

static func

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

Enums

A rich notion, allowing per-enum type information

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

Can also have methods

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}
impl Message {
    fn call(&self) {
        // method body would be defined here
    }
}
let m = Message::Write(String::from("hello"));
m.call();

The Option enum is very prevalent and replaces null

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

Use match (which is exhaustive) to process enums

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

"_" placeholder offers default

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

"if let" allows concise matching of one case

let some_u8_value = Some(0u8);​
if let Some(3) = some_u8_value {
    println!("three");
}

Modules

You can nest

mod network {
    fn connect() {
    }
    mod client {
        fn connect() {
        }
    }
}

A summary of the rules of modules with regard to files:

  • If a module named foo has no submodules, you should put the declarations for foo in a file named foo.rs.
  • If a module named foo does have submodules, you should put the declarations for foo in a file named foo/mod.rs.

Use pub for public visibility on modules and functions

Namespacing: use super to avoid root-based paths

Collection Classes

Vectors

&v for normal

let v = vec![100, 32, 57];
for i in &v {
    println!("{}", i);
}

&mut v for in-place changes

let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50;
}

Use enum to get multi-typed contents

Strings

UTF-8 so not a char-array

char-iteration

for c in "नमस्ते".chars() {
    println!("{}", c);
}

byte-iteration

for b in "नमस्ते".bytes() {
    println!("{}", b);
}

Hash Maps

Main points:

  • takes ownership of contained items
  • can iterate via "for (key, value) in &scores {"
  • use insert to overwrite, and entry(key).or_insert(value) to fill conditionally

Can mutate as we iterate:

use std::collections::HashMap;

let text = "hello world wonderful world";

let mut map = HashMap::new();

for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}

println!("{:?}", map);

Error Handling

panic!

  • Will ditch the program
  • Unwinds by default, but can set to abort in Cargo.toml
  • Use "RUST_BACKTRACE=1" to get a backtrace

Result


enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • "unwrap" for we-expect-this-to-work-and-generic-panic-is-ok
  • "expect" for more custom panic message
  • Use "?" as shorthand for propagating Result
use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
}

When To Use What

Use panic:

  • examples
  • protoypes
  • tests
  • some assumption, guarantee, contract, or invariant has been broken
  • e.g. preconditions on type constructors

Generics

The basic idea is familiar:

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());
}

Traits

Like C++ concepts

pub trait Summarizable {
    fn summary(&self) -> String;
}

Can have default implementations

Can then use in generics

pub fn notify<T: Summarizable>(item: T) {
    println!("Breaking news! {}", item.summary());
}

Can do partial matching so only those eligible can use the method

use std::fmt::Display;

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

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self {
            x,
            y,
        }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

Can also do "blanket implementations" for every type satisfying the trait(s):

impl<T: Display> ToString for T {
    // --snip--
}

Lifetimes

Basic Syntax

Apostrophe followed by (short) name

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

Example - here the result is only usable when both the inputs are usable

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

If a struct is using a borrowed data type, that will need a lifetime

struct ImportantExcerpt<'a> {
    part: &'a str,
}

Lifetimes and generics are both specified in angle brackets for types

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
    where T: Display
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Compiler Elision

The compiler does a lot of lifetime elision compared to pre-1.0 Rust

  • Each parameter that is a reference gets its own lifetime parameter. In other words, a function with one parameter gets one lifetime parameter: fn foo<'a>(x: &'a i32), a function with two arguments gets two separate lifetime parameters: fn foo<'a, 'b>(x: &'a i32, y: &'b i32), and so on.
  • If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters: fn foo<'a>(x: &'a i32) -> &'a i32.
  • If there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, then the lifetime of self is assigned to all output lifetime parameters. This makes writing methods much nicer.

Static

There is a special full-program lifetime: 'static

let s: &'static str = "I have a static lifetime.";

Tests

Writing Tests

Tests fail with panic!

Use assert!, assert_eq!, assert_ne!

Put tests inside modules, at the bottom

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

Use #[should_panic(expected="expected text")] attribute if code should panic

Running Tests

Run with "cargo test"

By default will run parallel, use "cargo test -- --test-threads=1" to set to single

Use "cargo test -- --nocapture" to see stdout

Use "cargo test xx" to run all tests with "xx" in their name

Use "cargo test -- --ignored" to include tests with the #[ignore] attribute

Test Organisation

Tests can access private functions in the same module (via standard Rust module rules)

Integration test: add "tests" directory alongside "src" and everything there with #[test] attribute will be run by "cargo test"

Each module in "tests" is compiled as a separate crate

To run a given integration test module, do "cargo test --test module_name"

Put shared (non-test) routines in "tests/common/mod.rs" rather than "tests/common.rs"

You can't run tests on binary crates. This implies binary crates should be trivial stubs that invoke the library crates.

Closures

Similar to functions:

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

An example of a simple cache closure:

struct Cacher<T>
    where T: Fn(u32) -> u32
{
    calculation: T,
    value: Option<u32>,
}
impl<T> Cacher<T>
    where T: Fn(u32) -> u32
{
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            calculation,
            value: None,
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            },
        }
    }
}

// Then call with a closure like this...

    let mut expensive_result = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    });

Instead of "Fn", can use "FnOnce" for single capture, "FnMut" for mutable borrowing -> can change environment.

Can force closure to take ownership with "move" keyword;

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

    let equal_to_x = move |z| z == x;

    // will not compile as ownership has been passed on
    println!("can't use x here: {:?}", x);

    let y = vec![1, 2, 3];

    assert!(equal_to_x(y));
}

Iterators

Usage:

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

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

Trait to implement:

trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // methods with default implementations elided
}

Methods: map, filter, collect, ...

Simple example:

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;

        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
}

#[test]
fn calling_next_directly() {
    let mut counter = Counter::new();

    assert_eq!(counter.next(), Some(1));
    assert_eq!(counter.next(), Some(2));
    assert_eq!(counter.next(), Some(3));
    assert_eq!(counter.next(), Some(4));
    assert_eq!(counter.next(), Some(5));
    assert_eq!(counter.next(), None);
}

Exmaple of filter usage:

// before...

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

// after...

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents.lines()
        .filter(|line| line.contains(query))
        .collect()
}

Targets

2nd edition book

Links

Tasks

  • 1. Introduction [DONE]
    • 1.1. Installation
    • 1.2. Hello, World!
  • 2. Guessing Game Tutorial [DONE]
  • 3. Common Programming Concepts
    • 3.1. Variables and Mutability
    • 3.2. Data Types
    • 3.3. How Functions Work
    • 3.4. Comments
    • 3.5. Control Flow
  • 4. Understanding Ownership [DONE]
    • 4.1. What is Ownership?
    • 4.2. References & Borrowing
    • 4.3. Slices
  • 5. Using Structs to Structure Related Data [DONE]
    • 5.1. Defining and Instantiating Structs
    • 5.2. An Example Program Using Structs
    • 5.3. Method Syntax
  • 6. Enums and Pattern Matching [DONE]
    • 6.1. Defining an Enum
    • 6.2. The match Control Flow Operator
    • 6.3. Concise Control Flow with if let
  • 7. Modules [DONE]
    • 7.1. mod and the Filesystem
    • 7.2. Controlling Visibility with pub
    • 7.3. Referring to Names in Different Modules
  • 8. Common Collections [DONE]
    • 8.1. Vectors
    • 8.2. Strings
    • 8.3. Hash Maps
  • 9. Error Handling [DONE]
    • 9.1. Unrecoverable Errors with panic!
    • 9.2. Recoverable Errors with Result
    • 9.3. To panic! or Not To panic!
  • 10. Generic Types, Traits, and Lifetimes
    • 10.1. Generic Data Types [DONE]
    • 10.2. Traits: Defining Shared Behavior [DONE]
    • 10.3. Validating References with Lifetimes [DONE]
  • 11. Testing [DONE]
    • 11.1. Writing tests
    • 11.2. Running tests
    • 11.3. Test Organization
  • 12. An I/O Project: Building a Command Line Program [DONE]
    • 12.1. Accepting Command Line Arguments
    • 12.2. Reading a File
    • 12.3. Refactoring to Improve Modularity and Error Handling
    • 12.4. Developing the Library’s Functionality with Test Driven Development
    • 12.5. Working with Environment Variables
    • 12.6. Writing Error Messages to Standard Error Instead of Standard Output
  • 13. Functional Language Features in Rust [DONE]
    • 13.1. Closures
    • 13.2. Iterators
    • 13.3. Improving our I/O Project
    • 13.4. Performance
  • 14. More about Cargo and Crates.io
    • 14.1. Customizing Builds with Release Profiles
    • 14.2. Publishing a Crate to Crates.io
    • 14.3. Cargo Workspaces
    • 14.4. Installing Binaries from Crates.io with cargo install
    • 14.5. Extending Cargo with Custom Commands
  • 15. Smart Pointers
    • 15.1. Box<T> Points to Data on the Heap and Has a Known Size
    • 15.2. The Deref Trait Allows Access to the Data Through a Reference
    • 15.3. The Drop Trait Runs Code on Cleanup
    • 15.4. Rc<T>, the Reference Counted Smart Pointer
    • 15.5. RefCell<T> and the Interior Mutability Pattern
    • 15.6. Creating Reference Cycles and Leaking Memory is Safe
  • 16. Fearless Concurrency
    • 16.1. Threads
    • 16.2. Message Passing
    • 16.3. Shared State
    • 16.4. Extensible Concurrency: Sync and Send
  • 17. Is Rust an Object-Oriented Programming Language?
    • 17.1. What Does Object-Oriented Mean?
    • 17.2. Trait Objects for Using Values of Different Types
    • 17.3. Object-Oriented Design Pattern Implementations
  • 18. Patterns Match the Structure of Values
    • 18.1. All the Places Patterns May be Used
    • 18.2. Refutability: Whether a Pattern Might Fail to Match
    • 18.3. All the Pattern Syntax
  • 19. Advanced Features
    • 19.1. Unsafe Rust
    • 19.2. Advanced Lifetimes
    • 19.3. Advanced Traits
    • 19.4. Advanced Types
    • 19.5. Advanced Functions & Closures
  • 20. Final Project: Building a Multithreaded Web Server
    • 20.1. A Single Threaded Web Server
    • 20.2. How Slow Requests Affect Throughput
    • 20.3. Designing the Thread Pool Interface
    • 20.4. Creating the Thread Pool and Storing Threads
    • 20.5. Sending Requests to Threads Via Channels
    • 20.6. Graceful Shutdown and Cleanup