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
, unsignedu8
-u128
, smaller number of bits is faster to process, e g.-10
as ani8
is way shorter than asi128
,isize
andusize
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
- integer: signed
- 2 compounds: array (same types), tuple (different types)
- 4 scalars:
- 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 untilbreak
, return value follows behindwhile
: 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 orTryFrom
/TryInto
traits - convert to String by implementing
Display
and usingto_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 => useclone
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 functionfill_vec
(line 6), so it gets dropped at the end offill_vec
, so we can't usevec0
again back inmain
after thefill_vec
call -
we can fix this in a few ways:
- Make a separate version of the data in
vec0
and pass that tofill_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 ownedVec<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 ofvec1
entirely
- Make a separate version of the data in
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 borrowself
- associated function: does not use self, e.g. for constructors
Enums and Pattern Matching
- enum for predefined options
- no
null
values Option<T>
, eitherSome(T)
orNone
Option<T>
andT
are different types, so we can't handleOption<T>
like a definitelyT
- 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 likeswitch
in JSmatch
is exhaustive, so every case must get handled_
to handle all other casesif 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 cratesrc/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 withuse
- 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()
orvec![]
- 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
andString
str
: dynamically sized, string slice, borrowed as&str
, simple, fast, pointer&
goes to stack, data to heapString
: dynamically sized, mutable, owned, UTF-8, slower, a pointer to the heap, owned- string literal is a
&str
String
isVec<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()
orString::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!
andResult<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
withOk
andErr
- more advanced:
unwrap
orexpect
- propagation with
?
for functions that returnResult
- 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 stopexpect
is slightly nicer, with a custom message?
propagates the error up, with no crashmatch
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 functionlargest
is generic over some typeT
and has one parameter namedlist
, which is a slice of values of typeT
and returns a value of the same typeT
"- 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> {
, butimpl 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>
becomesOption_i32
andOption_f64
- trait:
- functionality a particular type has and can share with other types
- similar to interfaces
- e.g. we have a
news
struct and atweet
struct and both need aSummary
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:
-
- arrange: setup data/state
-
- act: run the code to test
-
- assert: check if the results are what you expect
-
- terms:
cargo run
,#[test]
,assert_eq!
,#[should_panic]
- compared values need
PartialEq
andDebug
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 withcfg(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 iteratorargs
invec<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) andlib.rs
(program logic) - concerns for
main.rs
: call cli parsing, set up config, callrun
inlib.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 thenew
function ofConfig
- add error handling by returning
Result<T, E>
- extract logic from
main
intorun
function - return
Result<T, E>
and pass up error with?
, then useif let
to check for error - move logic into
lib.rs
and importConfig
and prefixrun
withminigrep::
- 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: needsItem
type andnext
function - iterator is mutable by design and consumes itself, the for loop takes ownership
iter
produces immutable referencesiter_mut
produces mutable referencesinto_iter
produces owned values- e.g.
sum
takes ownership, callsnext
, 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 andposition
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 structurecargo 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
andVec<T>
are smart pointers- smart pointers are usually implemented using structs
- they implement the
Deref
andDrop
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 forreference 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>
andRc<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 implementsDeref
deref
returns a reference, otherwise the value would be moved out ofself
- 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 useDerefMut
- allows to customize the behavior of
- 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 explicitdrop
would lead to double free error)
- allows to customize what happens when a value goes out of scope, e.g.
- reference cycles can leak memory (which are memory safe)
- Summary:
Rc<T>
enables multiple ownersBox<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 threadmove
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
- basic ones:
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 outputPartialEq
and Eq for equality comparisonsPartialOrd
and Ord for ordering comparisonsClone
andCopy
for duplicating valuesHash
for mapping a value to a value of fixed sizeDefault
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 tomatch
withpanic!
?
is equal tomatch
withreturn 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 anApp
withroute
s 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 fileservice
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
- database table: plural,
Web API
- actix-web for the http server
map_err
converts theErr
value of aResult
from one type to another, leaving theOk
value unchanged, e.g. if we pass a function that converts a value of type E to type F, it will convertResult<T, E>
toResult<T, F>
; this is useful for passing through theOk
value and handling theErr
- 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
(takesfold(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
- (Finished: 2021-09-16) Rust Book
- (Finished: 2021-09-16) Rustlings
- (Finished: 2021-09-17) Rust Cli
- (Finished: 2021-09-18) Rust By Example
- (Finished: 2021-09-21) Practical Rust Projects
- (Finished: 2021-09-27) Practical Rust Web Projects
- (Finished: 2021-??-??) easy-rust: simple language intro
- fasterthanli
- rust-cookbook
- zero-to-production
- Rust OS
- rust-learning: a list of learning materials
- Rust for Rustaceans