Rust

Start: 2021-08-20

Last update: 2021-10-05

Common Concepts

  • statically typed: must know all types at compile time (but can infer a lot of types)
  • variables are immutable
  • variables have a scope and live only in a block ({})
  • types: no implicit coercion
  • primitive types:
    • 4 scalars:
      • integer: signed i8 - i128, unsigned u8 - u128, smaller number of bits is faster to process, e g. -10 as an i8 is way shorter than as i128, isize and usize depend on the computer architecture
      • float
      • boolean
      • char: every character has an unicode number, e.g. A is number 65, all chars use 4 bytes of memory, but in a string least amount of memory
    • 2 compounds: array (same types), tuple (different types)
  • custom types:
    • structs: classic struct, tuple struct, unit struct
    • enums: allow only some variants, you can use type aliases to refer to enums
    • constants: unchangeable
  • custom types from standard library:
    • Strings: see in its own chapter
    • Vector: growable,
    • optional type Option
    • error type Result
    • heap allocated pointer Box
  • shadowing vs. mut: can change type, used if you change a number a lot
  • vector is an growable array
  • hashmap like js object
  • each parameter needs explicit type annotation
  • statements don't return a value, expressions evaluate to a return
  • expressions do not include ending semicolons, then they become a statement
  • last expression is an implicit return, explicit with return
  • return always needs type in function signature
  • simple if/else if/else
  • condition must be a bool, truthy/falsey like in JavaScript
  • if is an expression, so we can use it in a let
  • loop: infinitelly until break, return value follows behind
  • while: specific case (instead of loop, if/else, break construct)
  • for in: mostly used, also with a range
  • shadowing to hide old vars with the same name
  • no implicit casting
  • cast with From/Into traits or TryFrom/TryInto traits
  • convert to String by implementing Display and using to_string
  • parse a String by using parse or turbofish
  • Filesystem: create, open, remove_file, read_lines, create_dir, remove_dir, read_dir

Ownership

  • stack faster than heap

  • stack always fixed size, so data with unknown size at compile time or dynamic go to the heap

  • stack and heap both available to the code at runtime

  • stack stores values in the order it gets them and removes them in the opposite order (LIFO)

  • heap gives you a certain amount of space by a pointer ("allocating on the heap"), but free memory has to get searched for

  • the pointer is fixed size, so it can go on the stack

  • for the data, you have to follow the pointer

  • pushing to the stack is faster than allocating to the heap, because no need to search for free space

  • accessing the stack is faster, because you have to follow the pointer if you use the heap

  • normally garbage collector or manual freeing, Rust uses ownership

  • to make memory safety guarantees without a garbage collector

  • borrow, slices, data in memory

  • the compiler checks set rules at compile time

  • each value has a variable called its owner

  • there can only be one owner at a time

  • when the owner goes out of scope, the value gets dropped

  • second String type, from

  • string literal is immutable, so hardcoded into the binary, so fast

  • String is mutable, so unknown size at compile time, so lives on the heap

  • we need a way of returning this memory when we are done with the String

  • without a garbage collector, we have to do it; too late, we waste memory; too early, we have an invalid variable

  • drop gets called at the closing curly bracket

  • ownership manages heap data

  • with simple values, we copy the data onto the stack

  • with a string, we make a copy of the memory address, both point to the same data on the heap

  • a copy of the data could be very expensive

  • double free error: if heap would get dropped, but two pointers referred to one heap address

  • shallow copy becomes a move: s1 was moved into s2, so no double free error => use clone to deep copy

  • with fixed values no need for clone, because everything just lives on the stack

  • data on the stack (fixed size) has a Copy trait, so an older variable is still usable after assignment

  • reference (&): if you want to use it again, in addition to any data returned from function

  • reference: because it does not own it, the value will not get dropped when reference goes out of scope

  • having references = function parameters borrowing

  • to modify borrowed: because references are immutable, we have to make it mutable

  • only one mutable reference per scope allowed => prevents data races at compile time

  • not allowed to combine mutable and immutable references

  • data race occurs: two pointers access the same data the same time, one writes, so no sync

  • dangling reference: data goes out of scope, but reference exists

  • slices: if data with context loses context, e.g. index of string, but string gets deleted

  • string slice: &s[a..b]; starting point and lenght get stored

  • string literals are string slices, type &str

  • other slices for array etc.

  • vec0 is being moved into the function fill_vec (line 6), so it gets dropped at the end of fill_vec, so we can't use vec0 again back in main after the fill_vec call

  • we can fix this in a few ways:

    • Make a separate version of the data in vec0 and pass that to fill_vec instead
    • Make fill_vec borrow its argument instead of taking ownership of it and then copy the data within the function in order to return an owned Vec<i32>
    • Make fill_vec mutably borrow its argument (which will need to be mutable), modify it directly, then not return anything. Then you can get rid of vec1 entirely

Structs

  • custom data type to name and package related values => does the data belong together?
  • looks like JavaScript object {} with key-value pairs
  • add struct methods with impl block, and borrow self
  • associated function: does not use self, e.g. for constructors

Enums and Pattern Matching

  • enum for predefined options
  • no null values
  • Option<T>, either Some(T) or None
  • Option<T> and T are different types, so we can't handle Option<T> like a definitely T
  • this helps to handle one very common problem: handling null as if it would not be null
  • everywhere that a value has a type that isn’t an Option<T>, you can safely assume that the value isn’t null.
  • match is like switch in JS
  • match is exhaustive, so every case must get handled
  • _ to handle all other cases
  • if let if we only care about one case

Managing Projects

  • crate: a binary or library
  • package: one or more crates; contains a Cargo.toml that describes how to build those crates
  • a package must contain 0 or 1 library crate and multiple binary crates, but at least 1 crate
  • src/main.rs is the crate root of a binary crate
  • src/lib.rs is the crate root of a library crate
  • other binary crate go into src/bin
  • use brings a path into scope
  • module: to organize code within a create
  • all items in inner scopes are private by default
  • idiomatic way is to bring parent in scope to know that it comes from the outside
  • add external packages: add to Cargo.toml and import it with use
  • standard library is shipped with Rust, but has to get imported too
  • import all functions with *, but be cautious!

Common Collections

  • collections can contain multiple values
  • always live on the heap, so they are dynamically-sized (size not known at compile-time unlike array and tuple)
  • vector:
    • Docs
    • a variable number of values next to each other
    • all values of same type
    • Create: Vec::new() or vec![]
    • Read: get, [] (panics)
    • Update: push
    • no mutable borrow: mutable data could lead to new memory location of borrow
    • for i in &v
    • dereference with * to mutate mutable reference
    • use enum to store different types in vector
  • string:
    • Docs
    • a collection of characters / bytes of text
    • 2 types: str and String
    • str: dynamically sized, string slice, borrowed as &str, simple, fast, pointer & goes to stack, data to heap
    • String: dynamically sized, mutable, owned, UTF-8, slower, a pointer to the heap, owned
    • string literal is a &str
    • String is Vec<u8> under the hood
    • UTF-8 uses either bytes, scalars or grapheme clusters (mostly what we think is a "character"
    • to read grapheme clusters, use an external crate
    • Create: String::new() or String::from("") or "".to_string()
    • Read: for c in "".chars(), but not indexable (due UTF-8, return type issues, expected O(1))
    • Update: push_str, s1 + &s2 (takes ownership of s1, appends copy of s2 and returns ownership of result), format!` for multiple strings
  • hashmap:
    • Docs
    • key-value pairs
    • names in other languages: map, object, hash table, dictionary
    • to look up by key instead of index as in vector
    • types with Copy trait and owned values will be moved into and owned by the hashmap
    • Create: HashMap::new()
    • Read: get
    • Update: insert (overwrite), entry.or_insert (only if not existing)

Error Handling

  • error handling before code will compile
  • 2 types: unrecoverable (bug?!) and recoverable (retry)
  • no exceptions, but panic! and Result<T, E>
  • unrecoverable:
    • bad things can happen, so show failure message, clean up and quit
    • use backtrace for further debugging
  • recoverable:
    • Result<T, E> as result
    • use (nested) match with Ok and Err
    • more advanced: unwrap or expect
    • propagation with ? for functions that return Result
  • returning Result as a good default choice
  • panic if code could end up in bad state (broken contract, not expected)
  • create own type to add checks, e.g. if number is between 1 and 100
  • unwrap internally calls a panic with no error message => quick and dirty, we get the value or a stop
  • expect is slightly nicer, with a custom message
  • ? propagates the error up, with no crash
  • match is best practice

Generic Types, Traits, and Lifetimes

  • abstract stand-ins for concrete types or other properties
  • to express behavior or relation to other things without knowing what will be in their place
  • e.g. find the largest item in a slice of i32 values and find the largest item in a slice if char values
  • to parameterize the type in a function, we need to name the type parameter, so we use T
  • fn largest<T>(list: &[T]) -> T { => "the function largest is generic over some type T and has one parameter named list, which is a slice of values of type T and returns a value of the same type T"
  • same goes for structs: struct Point<T, U> { x: T, y: U }
  • and enums: enum Result<T, E> { Ok(T), Err(E), }
  • and impl: impl<T> Point<T> {, but impl Point<i32> {
  • Rust implements generics so that the code is not slower
  • monomorphization: turning generic code into specific code by filling the concrete types
  • e.g. Option<T> becomes Option_i32 and Option_f64
  • trait:
    • functionality a particular type has and can share with other types
    • similar to interfaces
    • e.g. we have a news struct and a tweet struct and both need a Summary trait; we can setup a default implementation and override it in each struct
    • we can use impl Trait or Trait bound syntax
  • lifetime:
    • rust's most distinctive feature
    • every reference has a lifetime, mostly implicit and inferred
    • goal is to prevent dangling references
    • borrow checker to check if all borrows are valid
    • each function that uses references need to specify lifetimes
    • lifetime annotations describe the relationship of the lifetimes to each other w/o affecting the lifetime
    • e.g. &'a i32
    • struct definitions can also hold lifetime annotations
    • lifetime elision rules: 1 input lifetime rule (parameters), 2 output lifetime rules (returns)
    • static for lifetime as long as the whole program

Writing Automated Tests

  • Rust thinks in unit tests (small, isolated) and integration tests (big, working together)
  • Rust high amount of concern about correctness
  • type system big part, but can't catch every kind of incorrectness
  • testing to test for logical (note: find better term) errors
  • 3 steps:
      1. arrange: setup data/state
      1. act: run the code to test
      1. assert: check if the results are what you expect
  • terms: cargo run, #[test], assert_eq!, #[should_panic]
  • compared values need PartialEq and Debug traits
  • custom messages as optional argument
  • also able to test for panic, but sometimes imprecise due different panics
  • also use Result<T, E>
  • tests run in parallel (= fast), so they shouldn't depend on each other, don't have shared state etc.
  • we can filter tests, e.g. by function name or module name, and also ignore tests
  • unit tests test for private implementatiosn details, go into each file they are testing, in a tests module with cfg(test) annotation
  • integration tests test mainly for public API, go into tests directory, each file is a separate crate
  • setups for integration tests go into a subdirectory of tests, e.g. tests/common/mod.rs

An I/O Project: Building a Command Line Program

  • we build a cli application, our own implementation of grep
  • we use newly learned knowledge from chapter 7, 8, 9, 10, 11
  • create new cratewe use newly learned
  • use std::env to read from environment
  • use collect to collect iterator args in vec<String>
  • use std::fs to read the file
  • problems: main parses cli args and reads file, different kind of tasks for vars, bad error message
  • separation of concerns: split program into main.rs (run program) and lib.rs (program logic)
  • concerns for main.rs: call cli parsing, set up config, call run in lib.rs, handle errors
  • cli parsing: own helper function, move args into correct data structure
  • Rustaceans normally avoid using clone to fix ownership problems, because of its runtime cost; but there are some pros and cons to it
  • relate specific functions to struct, e.g. parse_config can become the new function of Config
  • add error handling by returning Result<T, E>
  • extract logic from main into run function
  • return Result<T, E> and pass up error with ?, then use if let to check for error
  • move logic into lib.rs and import Config and prefix run with minigrep::
  • use TDD to implement search functionality to keep test coverage high
  • enviornment variables, e.g. if env var is on, then ignore casing
  • write errors to sterr instead of stdout by sending errors to file, so errors also show up in stdout

Functional Language Features: Iterators and Closures

  • significantly influenced by functional programming
  • a way to clearly express highlevel ideas at low-level performance
  • closures:
    • anonymous functions saved in a variable or passed as arguments
    • can capture values from the scope in which they are defined
    • refactoring of expensive_calculation into a closure that holds the function (NOT the result)
    • no need for type annotations, because no exposed interface: let add_one_v4 = |x| x + 1;
    • solution: create struct with closure and result, use memoization to store result and run only when needed, also use hashmap, so that we can override struct value
    • closures can capture their environment, function can not (scoped by curly brackets)
    • Fn: borrows immutably, FnMut: borrows mutably, FnOnce: takes ownership once
  • iterators:
    • perform tasks on a sequence of items
    • iterators are lazy, so they don't run until you call them
    • without iterators, you would e.g. use a normal for loop with index
    • all iterators implement the Iterator trait: needs Item type and next function
    • iterator is mutable by design and consumes itself, the for loop takes ownership
    • iter produces immutable references
    • iter_mut produces mutable references
    • into_iter produces owned values
    • e.g. sum takes ownership, calls next, consumes the iterator
    • other methods in the Iterator trait are called iterator adapters and we can chain them
    • because iterators are lazy, we have to call one consuming adaptor, e.g. collect
    • we can implement the Iterator trait on our own data structures
    • iterators (higher level) equally as fast as for loop (lower level) due zero-cost abstraction, because they get compiled down to roughly the same code
    • find to find value and position for index

More about Cargo and Crates.io

  • release profiles are predefined prfiles with different configs
  • cargo has two default profiles: dev and release
  • control it in Cargo.toml
  • crates.io to use other peoples code and publish your own code
  • // for code comments, /// for documentation commentsi, //! for crate comments
  • documentation comments can include tests, runnable with cargo test
  • pub use to re-export types at the top level for more useful public API structure
  • cargo doc to build documentation
  • metadata for crate goes into Cargo.toml
  • workspace as a set of packages
  • all crates in the workspace will use the same dependencies to be compatible with each other
  • Regular comments which are ignored by the compiler:
    • // Line comments which go to the end of the line.
    • /* Block comments which go to the closing delimiter. */
  • Doc comments which are parsed into HTML library documentation:
    • /// Generate library docs for the following item.
    • //! Generate library docs for the enclosing item.

Smart Pointers

  • a pointer is a concept for a variable that contains an address in memory
  • Rust's most common kind of pointer is a reference, that always borrows data
  • a smart pointer is a data structure that acts like a pointer but also has metadata and extra capabilities
  • e.g. the reference counting smart pointer to have multiple owners of data
  • smart pointers can own data, in contrast to a normal reference
  • String and Vec<T> are smart pointers
  • smart pointers are usually implemented using structs
  • they implement the Deref and Drop trait
  • Deref to act like a reference, Drop to customize the code that is run when the instance goes out of scope
  • most used smart pointers: Box<T>, Rc<T>, Ref<T>
  • Box<T>:
    • allows to store data on the heap instead of the stack, stack only has the pointer to the heap data
    • no performance overhead
    • usage: when you have a type whose size can't be known at compile time; when you want to transfer ownership without copying; when you want to own a value and care only that it is a type that implement a specific trait
    • enabling recursive types with boxes, where a value can have as part of itself another value of the same type; this could be infinitely, so Rust doesn't know how much space a value needs, e.g. in a Cons List
    • the list has an element and itself; by using a Box<T> with a pointer, the next cons list element is always the same size and we've broken the infinite, recursive chain
  • Rc<T>:
    • sometimes a value might have multiple owners, e.g. a node in a graph
    • rc stands for reference counting
    • keeps track of the number of references
    • via immutable references it allows to share data between multiple parts of the program for reading only
  • RefCell<T>:
    • a design pattern to mutate data even when there are immutable references to that data
    • difference to Box<T>: borrowing rules are enforced at runtime instead of compile time
    • combination of RefCell<T> and Rc<T> to have data with multiple owners and mutate it
  • Deref:
    • allows to customize the behavior of \*
    • we can't compare a value and a reference to a value because of different types, so we have to dereference
    • \*y actually means \*(y.deref()), so we don't have to know if it is a regular reference or a type that implements Deref
    • deref returns a reference, otherwise the value would be moved out of self
    • deref coercion happens when we pass a reference to a type's value as an argument to a function that does not match the parameter type, e.g. from &String to &str
    • to override the \* operator on mutable references, we use DerefMut
  • Drop:
    • allows to customize what happens when a value goes out of scope, e.g. Box<T> deallocates the heap space
    • variables are dropped in reverse order of their creation
    • std::mem::drop to drop a value (own explicit drop would lead to double free error)
  • reference cycles can leak memory (which are memory safe)
  • Summary:
    • Rc<T> enables multiple owners
    • Box<T> allows immutable or mutable borrows checked at compile time, Rc<T> allows only immutable borrows checked at compile time, RefCell<T> allows immutable or mutable borrows checked at runtime
    • you can mutate the value inside RefCell<T> even when it is immutable

Fearless Concurrency

  • concurrent programming: different parts of a program execute independently
  • parallel programming: different parts of a program execute at the same time
  • by leveraging ownership and type checking, many concurrency errors are compile-time errors in Rust
  • OS run program's in a process and the OS manages multiple processes at once
  • programs can run independent parts simultaneously by using threads
  • splitting the computation can improve performance and increase complexity
  • possible problems:
    • race conditions: inconsistent order of accessing reources
    • deadlock: multiple threads wait for each other to finish using a resource, preventing each other from continuing
    • bugs that happen only in specific situations, that are hard to debug
  • Rust uses 1:1 threading, where it calls the OS API to create threads in a 1:1 manner, to have a smaller language runtime
  • thread::spawn to create a new thread
  • move allows us to use data from one thread in another one
  • option 1: message passing:
    • to transfer data between threads by using channels
    • a channel has a transmitter and a receiver
    • multiple producers, single consumer
  • option 2: shared-state concurrenty:
    • uses mutex
    • only one thread allowed to access some data at any given time
    • uses locks
    • like managing the microphone on a panel discussion
    • `Arc: atomic Rc that is safe to share across threads
    • allow transference of ownership between threads with Send
    • allow access from multiple threads with Sync

Object Oriented Programming Features of Rust

  • structs and enums have data, impl provides methods on them
  • encapsulation by using pub and everything else is by default private
  • Rust has no inheritance, but traits to share code
  • Rust uses generics to abstract over different possible types and trait bounds (bounded parametric polymorphism)
  • use trait objects that allow for values of different types
  • by using trait objects we never have to check whether a value implements a specific method, because the compiler does this for us
  • object safety is require for trait objects
  • we can use the state pattern to encapsulate different kinds of behavior
  • but OOP patterns won't always be the best way to use Rust

Patterns and Matching

  • special syntax for matching against the structure of types
  • places to use patterns: match (exhaustive), if let (not exhaustive), while let, for, let (!), function parameters
  • refutable: can fail to match, e.g. Some(x)
  • irrefutable: match for any possible value, e.g. let x = 1;
  • function parameters, let and for can only accept irrefutable patterns
  • matching literals
  • matching named variables
  • multiple patterns with |
  • matching ranges with ..=
  • destructuring to break apart values
  • ignoring values with _ (no binding), _x (with binding), .. (multiple values)
  • match guards with if in arm
  • @ bindings

Advanced Features

  • unsafe Rust:
    • underlying computer hardware is inherently unsafe
    • unsafe rust: by nature, static analysis is conservative
    • unsafe does not turn off any safety check (e.g. the borrow checker)
    • enclose unsafe code within a safe abstraction and provide a safe API
    • superpowers: dereference a raw pointer, call an unsafe function, access or modify a mutable static variable, implement an unsafe trait, access fields of unions
  • advanced traits:
    • specifying placeholder types in trait definitions
    • default generic type parameters and operator overloading
    • fully qualified syntax for disambiguation
    • using supertraits to require one trait's functionality within another trait
    • using the newtype pattern to implement external trits on external types
  • advanced types:
    • using the newtype pattern for type safety and abstraction
    • creating type synonyms with types aliases
    • the never type that never returns
    • dynamically sized types and the Sized trait
  • advanced functions and closures:
    • function pointers
    • returning closures
  • macros:
    • basic ones: format!, print!, eprint!
    • macros are code that write other code (metaprogramming)
    • macros are expanded before the compiler interprets the meaning of the code
    • custom #[derive] macros used on structs on enums
    • attribute-like macros define custom attributes usable on any item
    • function-like macros looking like function calls but operate on the tokens specified as their argument
    • format!: write formatted text to String
    • print!: same as format! but the text is printed to the console (io::stdout).
    • println!: same as print! but a newline is appended.
    • eprint!: same as format! but the text is printed to the standard error (io::stderr).
    • eprintln!: same as eprint!but a newline is appended.
    • todo!: not implemented yet
    • dbg!: print debug to cli

Final Project: Building a Multithreaded Web Server

  • steps:
    • learn about TCP and HTTP
    • listen for TCP connections
    • parse HTTP requests
    • create a HTTP response
    • improve server throughput with a thread pool
  • TCP and HTTP:
    • both are request-response protocols (client-server)
    • TCP: low-level, how information gets from one server to another
    • HTTP: on top of TCP, defining the contents of request and response
  • listen to requests with TcpListener that binds to an IP
  • read the request
  • reply with response
  • create thread pool to reply to multiple requests
  • write the client interface first
  • use channels to communicate between threads

Appendix

  • derivable traits:
    • Debug for programmer output
    • PartialEq and Eq for equality comparisons
    • PartialOrd and Ord for ordering comparisons
    • Clone and Copy for duplicating values
    • Hash for mapping a value to a value of fixed size
    • Default for default values
  • development tools:
    • formatting: rustfmt
    • fix code: rustfix
    • linting: clippy
    • IDE integration: rust language server

Other findings


Projects Built

CLI

  • unwrap is equal to match with panic!
  • ? is equal to match with return Err()
  • anyhow - provides anyhow::Error for easy error handling
  • assert_cmd - simplifies integration testing of CLIs
  • atty - detected whether application is running in a tty
  • clap-verbosity-flag - adds a --verbose flag to structopt CLIs
  • clap - command line argument parser
  • confy - boilerplate-free configuration management
  • convey - easy output for machines and humans
  • crossbeam-channel - provides multi-producer multi-consumer channels for message passing
  • ctrlc - easy ctrl-c handler
  • env_logger - implements a logger configurable via environment variables
  • exitcode - system exit code constants
  • human-panic - panic message handler
  • indicatif - progress bars and spinners
  • log - provides logging abstracted over implementation
  • predicates - implements boolean-valued predicate functions
  • proptest - property testing framework
  • serde_json - serialize/deserialize to JSON
  • signal-hook - handles UNIX signals
  • structopt - parses command line arguments into a struct
  • tokio - asynchronous runtime
  • wasm-pack - tool for building WebAssembly

Catsay CLI

  • binaries: single programs tha are used independently
  • libraries: building blocks for other programs
  • StructOpt:
    • combines clap (generates a parser for the arguments) and custom derive (generates a default implementation of a trait by annotating a struct)
    • you can define a struct containing the arguments you want and annotate it with #[derive(StructOpt)]
  • publishing:
    • cargo install: installs locally from the source code (rust toolchain needed)
    • crates.io: higher visibility, but also install from source code
    • cargo build: builds binary, which you can upload (no rust toolchain needed to run it)

Catsay TUI

  • how to use ncurses and cursive (TUI)
  • create root, add layers
  • The event loop is a fundamental concept in building user interfaces.
  • CLI interactions are usually limited to one input and one output. For user input, you have to pause the execution and wait for the user input to finish. No other operations or output can be processed at that time.
  • A GUI might be expecting multiple inputs, e.g. keypresses or mouse clicks on a button.
  • Since you can’t predict which input will be triggered first, the program runs in an infinite loop that handles whatever next input is triggered. If you have registered some event handler, the event loop will invoke the handler when the event happens. E.g. if you have an OK button in a dialog box, clicking it will trigger a button click event, which might be handled by a handler that closes this dialog box.
  • Cursive uses layers to create a stacked view of the components (e.g. Views). Layers are stacked together so that the top-most one will be active and can receive input.

Catsay GUI

  • how to use GTK+ (gtk-rs) and Glade
  • gtk-rs is a rust binding around the C-based GTK library
  • elements: Application, ApplicationWindow, Box, Label, Image
  • glade to build UI with XML

Webpage Server-Side Rendered

  • actix-web for the http server
  • the HttpServer serves an App with routes which invoke handlers depending on the request method; the server binds to a specific IP and port and awaits the requests
  • actix-web uses tokio under the hood
  • to serve multiple clients, we can create multiple processes (= multiple server instances), but this doesn't scale well
  • we can use multiple threads (parts of a process), but there is also some overhead
  • with async/await, we can serve multiple clients per threads, so it is faster, because it doesn't block
  • NamedFile to use a file
  • service e.g. to serve static folder (from actix-files)
  • handlebars as templating engine, serder_json to use json
  • diesel as ORM, postgresql as database
  • schema migration is a way to version-control your database schema (instead of using ad-hoc SQL commands), so you do not miss the ability to quickly roll back or rebuild the database from scratch. When using schema migration, you write a SQL script to apply the schema change and another script to revert the change. You can also bring an old database to the latest schema by applying all the migrations it missed. A migration tool will usually determine which migration needs to be applied, so you don’t have to worry about it
  • diesel:
    • database table: plural, models.rs: struct singular, schema.rs: autogenerated
    • order of fields in models should match database table

Web API

  • actix-web for the http server
  • map_err converts the Err value of a Result from one type to another, leaving the Ok value unchanged, e.g. if we pass a function that converts a value of type E to type F, it will convert Result<T, E> to Result<T, F>; this is useful for passing through the Ok value and handling the Err
  • actix-web errors:
    • ErrorBadRequest(): 400
    • ErrorNotFound(): 404
    • ErrorInternalServerError(): 500
    • ErrorBadGateway(): 502
  • add proper logging: error, warn, info, debug, trace

WebSocket Chat

  • how can the server start communication, e.g. for notifications?

  • types:

    • polling: the client periodically sends a request to the server to see if there are updates; most of the time there aren't any updates, so we waste a lot of data
    • long polling: the server holds the connection open until it has some data, but server now has responsibility of keeping track of open connections
    • server-sent events: the client connects to the server using the eventsource web api, but this is only unidirectional
    • websocket: provides bidirectional full-duplex communication over TCP
  • websocket events:

    • open: Connection established
    • message: Received message from the server
    • error: Error happened
    • close: Connection closed
  • messages are not simple strings, but frames

Web Frontend JS/Rust

  • wasm is an open standard for binaries for near-native performance in browsers
  • we can write rust and compile it to wasm
  • wasm does not replace javascript
  • use wasm-pack and wasm-pack-template for the tooling
  • javascript calls wasm, then wasm does stuff

Web Frontend Pure Rust

  • yew as frontend tool, uses wasm-pack and webpack
  • yew uses concepts of React and Elm (Model, View, Update

From Katas

  • convert to iterator, useful for filter, map: iter
  • check if positive: is_positive
  • math stuff: sum, product
  • range: is between 1 and 3 (inclusive)? 1..=3
  • reduce function: fold (takes fold(init, |a, b| fn)
  • char in string: chars().nth(), chars().next(), chars().last()
  • iterate over string: chars()
  • convert a string slice in a given base to an integer: from_str_radix
  • sort and reverse iterator: sort, reverse

Interesting Topics / Questions

  • stack/heap
  • borrows
  • raw pointer
  • error handling
  • is shadowing best practice? duplicate names, harder to debug?
  • mutable references
  • structs
  • traits
  • lifetime
  • concurrency
  • what happens when the used integer is more than my computer architecture, e.g. i128 on a 64bit system? answer

Project Ideas

  • Given a list of integers, use a vector and return the mean (the average value), median (when sorted, the value in the middle position), and mode (the value that occurs most often) of the list.
  • Convert strings to pig latin. The first consonant of each word is moved to the end of the word and “ay” is added, so “first” becomes “irst-fay.” Words that start with a vowel have “hay” added to the end instead (“apple” becomes “apple-hay”).
  • Create a text interface to allow a user to add employee names to a department in a company. For example, “Add Sally to Engineering” or “Add Amir to Sales.” Then let the user retrieve a list of all people in a department or all people in the company by department, sorted alphabetically.

Links