Traits and Threads Workshop

These are raw notes taken at RustConf during the “Traits and Threads” workshop put on by Aaron Turon

All Exercises

  • Methods are function that accept self
  • &self.items == &(self.items) where the borrow (&) is for the item, not for self.
  • Structs have no default value, requires creating all values.
  • mut as mute not like a mixed breed dog

Abstraction: The Plan

  • Generics
  • Traits
    • As Interfaces
    • For Code Resuse
    • For Perator Overloading
  • Trait Objects

Traits are rusts interfaces.

trait Print {
    fn print(&self);
}

impl Print for u64 {
    fn print(&self) { println!("{}", self) }
}

impl Print for char {
    fn print(&self) { println!("'{}'"), self) }
}

Question:
What happens if you try to define the same trait or impl that is defined already by someone else? (naming collisions etc)

Answer:
Naming collisions follow normal examples but when you try to write an interface for a type that you don’t own (code gluing) you will run in to Coherence issues, for any given trait and type there is a single implementation known to the compiler.


Question:
Why does the compiler allow “different” match arms?

Answer:
Given the below code: total += price is an expression and return None Breaks out of the function, so the compiler “assigns” it the type you need. It’s a “get out of jail free card.”

The Exercise:

let mut total = 0.0;

for item in shopping_list {
    match provider.price(item) {
        Some(price) => total += price,
        None => return None
    }
}

Some(total)

Reuse: Default Methods

Questions: Should we write methods, or functions?

When in doubt, lean on methods. Writing things as methods instead of functions allows for writing different impls for those methods.


Reuse: Layering

trait Clone {
    fn clone(&self) -> Self;
}

impl<T: Clone, U: Clone> Clone for (T, U) {
    fn clone(&self) -> (T, U) {
        (self.0.clone(), self.1.clone())
    }
}

impl<T: Clone> Clone for Vec<T> {  }

Question:
We’re using traits in places where we could use types, what is the relationship between traits and types?

Answer:
We’re not yet using traits in a place where types are expected. We’re saying T: is an addon that something has to be true for the type that we’re given. Traits are not types, they are information about types.


Question:
Why T & U?

Answer:
T for type, and U because it’s the next letter in the alphabit.


Question:
Could I defined a generic that says it can not have a specific trait.

Answer:
In most cases you wouldn’t need to although it is possible using some pretty advanced techniques.


Objects

Containers are homogeneous. Traits can kind of be used as types.

trait Print {
    fn print(&self);
}

fn print_slice(slice: &[&Print]) {
    for elem in slice { elem.print(); }
}

fn main() {
    let slice: &[&Print] = &...
}

[&Print] is actually an array of pointers, as pointers are all the same sized. vs a Char and Item which would be mixed sizes. The compiler will perform this type of abstraction for you.

Generics vs Objects

Generics ( aka zero cost abstractions)

  • Work for singletons
  • Work for uniform collection
  • Provide static dispatch
    • Good for performance

Objects

  • Must live behind a pointer
  • Work for heterogeneous collections
  • Provide dynamic dispatch
    • Bad for performance

Threads

Moden Parallel systems programming without the hassle, crashes, heisenbugs, fear.

Multiparadigm

  • Message passing
  • Mutable shared memory

Principals of compiler design

No data races, No matter what, regardless of paradigm.

What are data races?
Two unsynchronized threads accessing the same data where at lease one writes.

Ingredients of a data race:

  • Aliasing (multiple things with access to the same thing)
  • Mutation (at least one thread is writing)
  • No ordering (compiler has no idea this is happening and will optimize assuming it’s not happening)

Ownership and borrowing system is at the core of ensuring you can have both aliasing or mutation, but never at the same time. No data races = No accidentally-shared state.

All sharing is explicit. No threads can have access to this thing while your thread has access to it.

Each thread will move and take ownership of variables passed to that thread. Usage in the main thread is no longer permitted, and you’ll get a compile time error.

Sharing immutable data between threads?
Use std::sync::Arc. With Arc you can not know at what point it will be deallocated. It eliminates itself once all threads have finished with itself. Arc keeps a count for how much it’s been cloned and when it’s dropped it decrements and eliminates itself once at 0.

Static lifetime is exempt and allowed to be passed to different threads.

Channels

Two threads can transfer ownership of data through a channel. Channels create a transmitter and a receiver. Tx and Rx can be passed to threads. Whoever has the Tx, can send data to whoever owns the Rx.

Locks

Mutex is a name for a lock. Mutual exclusion. Rust fundamentally uses ownership to manage unlocks of locks you’ve taken. You never need to unlock or let go of the data.