diff --git a/slides/.gitignore b/slides/.gitignore index 8863e7a1..7adb2511 100644 --- a/slides/.gitignore +++ b/slides/.gitignore @@ -6,3 +6,4 @@ dist-* tmp/ yarn-error.log */**/target +*/**/slides.md diff --git a/slides/A-foundations/basic-syntax.md b/slides/A-foundations/basic-syntax.md new file mode 100644 index 00000000..59466f79 --- /dev/null +++ b/slides/A-foundations/basic-syntax.md @@ -0,0 +1,545 @@ +--- + +# Variables + +```rust {all|2|all} +fn main() { + let some_x = 5; + println!("some_x = {}", some_x); + some_x = 6; + println!("some_x = {}", some_x); +} +``` + + + +
+ +```text +Compiling hello-world v0.1.0 (/home/101-rs/Projects/hello-world) +error[E0384]: cannot assign twice to immutable variable `some_x` +--> src/main.rs:4:5 + | +2 | let some_x = 5; + | ------ + | | + | first assignment to `some_x` + | help: consider making this binding mutable: `mut some_x` +3 | println!("some_x = {}", some_x); +4 | some_x = 6; + | ^^^^^^^^^^ cannot assign twice to immutable variable + +For more information about this error, try `rustc --explain E0384`. +error: could not compile `hello-world` due to previous error +``` + +
+ +
+ + + +--- + +# Variables + +```rust +fn main() { + let mut some_x = 5; + println!("some_x = {}", some_x); + some_x = 6; + println!("some_x = {}", some_x); +} +``` + + + +
+ +```text +Compiling hello-world v0.1.0 (/home/101-rs/Projects/hello-world) +Finished dev [unoptimized + debuginfo] target(s) in 0.26s +Running `target/debug/hello-world` +some_x = 5 +some_x = 6 +``` + +
+ +
+ + + +--- + +# Assigning a type to a variable + +```rust +fn main() { + let x: i32 = 20; +} +``` + +- Rust is strongly and strictly typed +- Variables use type inference, so no need to specify a type +- We can be explicit in our types (and sometimes have to be) + +--- +layout: two-cols +--- + +# Integers + +| Length | Signed | Unsigned | +|---------------|---------|----------| +| 8 bits | `i8` | `u8` | +| 16 bits | `i16` | `u16` | +| 32 bits | `i32` | `u32` | +| 64 bits | `i64` | `u64` | +| 128 bits | `i128` | `u128` | +| pointer-sized | `isize` | `usize` | + +- Rust prefers explicit integer sizes +- Use `isize` and `usize` sparingly + +::right:: + + + +# Literals + +```rust +fn main() { + let x = 42; // decimal as i32 + let y = 42u64; // decimal as u64 + let z = 42_000; // underscore separator + + let u = 0xff; // hexadecimal + let v = 0o77; // octal + let w = 0b0100_1101; // binary + let q = b'A'; // byte syntax (stored as u8) +} +``` + + + + + +--- + +# Floating points and floating point literals + +```rust +fn main() { + let x = 2.0; // f64 + let y = 1.0f32; // f32 +} +``` + +- `f32`: single precision (32-bit) floating point number +- `f64`: double precision (64-bit) floating point number + + + +--- + +# Numerical operations + +```rust +fn main() { + let sum = 5 + 10; + let difference = 10 - 3; + let mult = 2 * 8; + let div = 2.4 / 3.5; + let int_div = 10 / 3; // 3 + let remainder = 20 % 3; +} +``` + + + +- These expressions do overflow/underflow checking in debug +- In release builds these expressions are wrapping, for efficiency +- You cannot mix and match types here, not even between different integer +types + +```rust +fn main() { + let invalid_div = 2.4 / 5; // Error! + let invalid_add = 20u32 + 40u64; // Error! +} +``` + + + + + +--- + +# Booleans and boolean operations + +```rust +fn main() { + let yes: bool = true; + let no: bool = false; + let not = !no; + let and = yes && no; + let or = yes || no; +} +``` + +--- + +# Comparison operators + +```rust +fn main() { + let x = 10; + let y = 20; + x < y; // true + x > y; // false + x <= y; // true + x >= y; // false + x == y; // false + x != y; // true +} +``` + +Note: as with numerical operators, you cannot compare different integer and +float types with each other + +```rust +fn main() { + 3.0 < 20; // invalid + 30u64 > 20i32; // invalid +} +``` + + + +--- + +# Characters + +```rust +fn main() { + let c = 'z'; + let z = 'ℤ'; + let heart_eyed_cat = '😻'; +} +``` + +- A character is a 32-bit unicode scalar value +- Very much unlike C/C++ where char is 8 bits + + + +--- + +# Strings +```rust + // Owned, heap-allocated string *slice* + let s1: String = String::new("Hello, 🌍!"); +``` + +- Rust strings are UTF-8-encoded +- Unlike C/C++: *Not null-terminated* +- Cannot be indexed like C strings +- Actually many types of strings in Rust + + + +--- +layout: three-slots +--- +# Tuples + +::left:: + +```rust +fn main() { + let tup: (i32, f32, char) = (1, 2.0, 'a'); +} +``` + +- Group multiple values into a single compound type +- Fixed size +- Different types per element +- Create a tuple by writing a comma-separated list of values inside parentheses + +::right:: + + + +```rust +fn main() { + let tup = (1, 2.0, 'Z'); + let (a, b, c) = tup; + println!("({}, {}, {})", a, b, c); + + let another_tuple = (true, 42); + println!("{}", another_tuple.1); +} +``` + +- Tuples can be destructured to get to their individual values +- You can also access individual elements using the period operator followed by + a zero based index + + + + + +--- + +# Arrays + +```rust +fn main() { + let arr: [i32; 3] = [1, 2, 3]; + println!("{}", arr[0]); + let [a, b, c] = arr; + println!("[{}, {}, {}]", a, b, c); +} +``` + +- Also a collection of multiple values, but this time all of the same type +- Always a fixed length at compile time (similar to tuples) +- Use square brackets to access an individual value +- Destructuring as with tuples +- Rust always checks array bounds when accessing a value in an array + + + +--- + +# Control flow + +```rust {all|3-10|4-9|8|13-16|18-20|all} +fn main() { + let mut x = 0; + loop { + if x < 5 { + println!("x: {}", x); + x += 1; + } else { + break; + } + } + + let mut y = 5; + while y > 0 { + y -= 1; + println!("y: {}", x); + } + + for i in [1, 2, 3, 4, 5] { + println!("i: {}", i); + } +} +``` + + + +--- + +# Functions + +```rust +fn add(a: i32, b: i32) -> i32 { + a + b +} + +fn returns_nothing() -> () { + println!("Nothing to report"); +} + +fn also_returns_nothing() { + println!("Nothing to report"); +} +``` + +- The function boundary must always be explicitly annotated with types +- Within the function body type inference may be used +- A function that returns nothing has the return type *unit* (`()`) +- The function body contains a series of statements optionally ending with an +expression + + + +--- + +# Statements +- Statements are instructions that perform some action and do not return a value +- A definition of any kind (function definition etc.) +- The `let var = expr;` statement +- Almost everything else is an expression + +## Example statements +```rust +fn my_fun() { + println!("{}", 5); +} +``` + +```rust +let x = 10; +``` + + + +```rust +let x = (let y = 10); // invalid +``` + + + + + +--- + +# Expressions + +- Expressions evaluate to a resulting value +- Expressions make up most of the Rust code you write +- Includes all control flow such as `if` and `while` +- Includes scoping braces (`{` and `}`) +- An expression can be turned into a statement by adding a semicolon (`;`) + +```rust {all|2-5} +fn main() { + let y = { + let x = 3; + x + 1 + }; + println!("{}", y); // 4 +} +``` + +--- + +# Expressions - control flow + +- Control flow expressions as a statement do not need to end with a semicolon +if they return *unit* (`()`) +- Remember: A block/function can end with an expression, but it needs to have +the correct type + +```rust {all|3-8|10-15} +fn main() { + let y = 11; + // if as an expression + let x = if y < 10 { + 42 + } else { + 24 + }; + + // if as a statement + if x == 42 { + println!("Foo"); + } else { + println!("Bar"); + } +} +``` + +--- + +# Scope + +- We just mentioned the scope braces (`{` and `}`) +- Variable scopes are actually very important for how Rust works + +```rust +fn main() { + println!("Hello, {}", name); // invalid: name is not yet defined + let name = "world"; // from this point name is in scope + println!("Hello, {}", name); +} // name goes out of scope +``` + +--- + +# Scope + +As soon as a scope ends, all variables for that scope can be removed from the +stack + +```rust +fn main() { // nothing in scope here + let i = 10; // i is now in scope + if i > 5 { + let j = 20; // j is now also in scope + println!("i = {}, j = {}", i, j); + } // j is no longer in scope, i still remains + println!("i = {}", i); +} // i is no longer in scope +``` + + \ No newline at end of file diff --git a/slides/A-foundations/closures.md b/slides/A-foundations/closures.md new file mode 100644 index 00000000..de6ccb30 --- /dev/null +++ b/slides/A-foundations/closures.md @@ -0,0 +1,30 @@ +--- +layout: default +--- +# Intermezzo: Closures + +- Closures are anonymous (unnamed) functions +- they can capture ("close over") values in their scope +- they are first-class values + +```rust +fn foo() -> impl Fn(i64, i64) -> i64 { + z = 42; + |x, y| x + y + z +} + +fn bar() -> i64 { + // construct the closure + let f = foo(); + + // evaluate the closure + f(1, 2) +} +``` + +- very useful when working with iterators, `Option` and `Result`. + +```rust +let evens: Vec<_> = some_iterator.filter(|x| x % 2 == 0).collect(); +``` + diff --git a/slides/A-foundations/composite-types.md b/slides/A-foundations/composite-types.md new file mode 100644 index 00000000..01dbd0a2 --- /dev/null +++ b/slides/A-foundations/composite-types.md @@ -0,0 +1,166 @@ +--- + +# Types redux +We have previously looked at some of the basic types in the Rust typesystem + +- Primitives (integers, floats, booleans, characters) +- Compounds (tuples, arrays) +- Most of the types we looked at were `Copy` +- Borrowing will make more sense when we look at some more ways we can type + our data + +--- + +# Structuring data +Rust has two important ways to structure data + +* structs +* enums +* ~~unions~~ + + + +--- + +# Structs +A struct is similar to a tuple, but this time the combined type gets its own name + +```rust +struct ControlPoint(f64, f64, bool); +``` + + + +This is an example of a *tuple struct*. You can access the fields in the struct +the same way as with tuples: + +```rust +fn main() { + let cp = ControlPoint(10.5, 12.3, true); + println!("{}", cp.0); // prints 10.5 +} +``` + + + + + +--- + +# Structs +Much more common though are structs with named fields + +```rust +struct ControlPoint { + x: f64, + y: f64, + enabled: bool, +} +``` + +* We can add a little more purpose to each field +* No need to keep our indexing up to date when we add or remove a field + + + + +```rust {all|2-6|7} +fn main() { + let cp = ControlPoint { + x: 10.5, + y: 12.3, + enabled: true, + }; + println!("{}", cp.x); // prints 10.5 +} +``` + + + + + +--- + +# Enumerations +One of the more powerful kinds of types in Rust are enumerations + +```rust +enum IpAddressType { + Ipv4, + Ipv6, +} +``` + +* An enumeration (listing) of different *variants* +* Each variant is an alternative value of the enum, you pick a single value to + create an instance + + + +```rust +fn main() { + let ip_type = IpAddressType::Ipv4; +} +``` + + + +--- + +# Enumerations +Enums get more powerful, because each variant can have associated data with +it + +```rust +enum IpAddress { + Ipv4(u8, u8, u8, u8), + Ipv6(u16, u16, u16, u16, u16, u16, u16, u16), +} +``` + +* This way, the associated data and the variant are bound together +* Impossible to create an ipv6 address while only giving a 32 bits integer + +```rust +fn main() { + let ipv4_home = IpAddress::Ipv4(127, 0, 0, 1); + let ipv6_home = IpAddress::Ipv6(0, 0, 0, 0, 0, 0, 0, 1); +} +``` + +* Note: an enum always is as large as the largest variant + + + +
+ + + + + + +
+ + diff --git a/slides/A-foundations/entry.md b/slides/A-foundations/entry.md new file mode 100644 index 00000000..82f2c647 --- /dev/null +++ b/slides/A-foundations/entry.md @@ -0,0 +1,35 @@ +--- +theme: default +class: text-center +highlighter: shiki +lineNumbers: true +info: "Rust - A1: Language basics" +drawings: + persist: false +fonts: + mono: Fira Mono +layout: cover +title: "Rust - A1: Language basics" +--- + +# Rust programming + +Module A: Foundations + +#[docdoc:path="why-rust.md"] +#[docdoc:path="first-project.md"] +#[docdoc:path="basic-syntax.md"] +#[docdoc:path="move-semantics.md"] +#[docdoc:path="ownership-borrowing.md"] +#[docdoc:path="composite-types.md"] +#[docdoc:path="pattern-matching.md"] +#[docdoc:path="impl-blocks.md"] +#[docdoc:path="optionals-errors.md"] +#[docdoc:path="vec.md"] +#[docdoc:path="slices.md"] +#[docdoc:path="traits-generics.md"] +#[docdoc:path="lifetime-annotations.md"] +#[docdoc:path="closures.md"] +#[docdoc:path="smart-pointers.md"] +#[docdoc:path="trait-objects.md"] +#[docdoc:path="interior-mutability.md"] diff --git a/slides/A-foundations/first-project.md b/slides/A-foundations/first-project.md new file mode 100644 index 00000000..fc0166d0 --- /dev/null +++ b/slides/A-foundations/first-project.md @@ -0,0 +1,81 @@ +--- +layout: default +--- + +# A new project + +```bash +$ cargo new hello-world +``` + + + +```bash +$ cd hello-world +$ cargo run +``` + + + + + +
+ +```text +Compiling hello-world v0.1.0 (/home/101-rs/Projects/hello-world) +Finished dev [unoptimized + debuginfo] target(s) in 0.74s +Running `target/debug/hello-world` +Hello, world! +``` + +
+ +
+ + +--- + +# Hello, world! + +```rust {all|1-3|2|5-11|6-10|7,9|all} +fn main() { + println!("Hello, world! fib(6) = {}", fib(6)); +} + +fn fib(n: u64) -> u64 { + if n <= 1 { + n + } else { + fib(n - 1) + fib(n - 2) + } +} +``` + + + +
+ +```text +Compiling hello-world v0.1.0 (/home/101-rs/Projects/hello-world) +Finished dev [unoptimized + debuginfo] target(s) in 0.28s +Running `target/debug/hello-world` +Hello, world! fib(6) = 8 +``` + +
+ +
+ + diff --git a/slides/A-foundations/impl-blocks.md b/slides/A-foundations/impl-blocks.md new file mode 100644 index 00000000..ff188092 --- /dev/null +++ b/slides/A-foundations/impl-blocks.md @@ -0,0 +1,102 @@ +--- + +# Intermission: Impl blocks +In the past few slides we saw a syntax which wasn't explained before: + +```rust {3} +fn main() { + let x = Some(42); + let unwrapped = x.unwrap(); + println!("{}", unwrapped); +} +``` + +* The syntax `x.y()` looks similar to how we accessed a field in a struct +* We can define functions on our types using impl blocks +* Impl blocks can be defined on any type, not just structs (with some limitations) + +--- + +# Intermission: Impl blocks + +```rust {all|6,13|7-12|7|17} +enum IpAddress { + Ipv4(u8, u8, u8, u8), + Ipv6(u16, u16, u16, u16, u16, u16, u16, u16), +} + +impl IpAddress { + fn as_u32(&self) -> Option { + match self { + IpAddress::Ipv4(a, b, c, d) => a << 24 + b << 16 + c << 8 + d + _ => None,_ + } + } +} + +fn main() { + let addr = IpAddress::Ipv4(127, 0, 0, 1); + println!("{:?}", addr.as_u32()); +} +``` + + + +--- + +# Intermission: Impl blocks, self and Self + +- The `self` parameter defines how the method can be used. +- The `Self` type is a shorthand for the type on which the current + implementation is specified. + +```rust {all|4-6|8-14|16-18} +struct Foo(i32); + +impl Foo { + fn consume(self) -> Self { + Self(self.0 + 1) + } + + fn borrow(&self) -> &i32 { + &self.0 + } + + fn borrow_mut(&mut self) -> &mut i32 { + &mut self.0 + } + + fn new() -> Self { + Self(0) + } +} +``` + +--- + +# Intermission: Impl blocks, the self parameter +The self parameter is called the *receiver*. + +* The self parameter is always the first and it always has the type on which it + was defined +* We never specify the type of the self parameter +* We can optionally prepend `&` or `&mut ` to self to indicate that we take + a value by reference +* Absence of a self parameter means that the function is an associated function + instead + +```rust +fn main () { + let mut f = Foo::new(); + println!("{}", f.borrow()); + *f.borrow_mut() = 10; + let g = f.consume(); + println!("{}", g.borrow()); +} +``` diff --git a/slides/A-foundations/interior-mutability.md b/slides/A-foundations/interior-mutability.md new file mode 100644 index 00000000..e69de29b diff --git a/slides/A-foundations/layouts b/slides/A-foundations/layouts new file mode 120000 index 00000000..00799ed8 --- /dev/null +++ b/slides/A-foundations/layouts @@ -0,0 +1 @@ +../layouts \ No newline at end of file diff --git a/slides/A-foundations/lifetime-annotations.md b/slides/A-foundations/lifetime-annotations.md new file mode 100644 index 00000000..e69de29b diff --git a/slides/A-foundations/move-semantics.md b/slides/A-foundations/move-semantics.md new file mode 100644 index 00000000..6927f17a --- /dev/null +++ b/slides/A-foundations/move-semantics.md @@ -0,0 +1,493 @@ +--- +layout: two-cols +--- +# Memory management + +- Most of what we have seen so far is stack-based and small in size +- All these primitive types are `Copy`: create a copy on the stack every time +we need them somewhere else +- We don't want to pass a copy all the time +- Large data that we do not want to copy +- Modifying original data +- What about data structures with a variable size? + +::right:: + + + +![Memory Layout](/images/A1-memory-expanded.svg) + + + + +--- +layout: section +--- +# Rust's ownership model + +--- +layout: default +--- +# Memory + +- A computer program consists of a set of instructions +- Those instructions manipulate some memory +- How does a program know what memory can be used? + + + +--- + +# Fundamentals + +There are two mechanisms at play here, generally known as the stack and the heap + +
+
+
+
Frame 1
+
Frame 2
+
+
+
Free memory
+
+
+
Heap
+
Allocated
+
+
+
+
+
+
🠔 Stack pointer
+
+
+ + + +--- + +# Fundamentals + +There are two mechanisms at play here, generally known as the stack and the heap + +
+
+
+
Frame 1
+
Frame 2
+
Frame 3
+
+
+
Free memory
+
+
+
Heap
+
Allocated
+
+
+
+
+
+
🠔 Stack pointer
+
+ A stack frame is allocated for every function call. It contains exactly + enough space for all local variables, arguments and stores where the + previous stack frame starts. +
+
+
+ + + +--- + +# Fundamentals + +There are two mechanisms at play here, generally known as the stack and the heap + +
+
+
+
Frame 1
+
Frame 2
+
+
+
Free memory
+
+
+
Heap
+
Allocated
+
+
+
+
+
+
🠔 Stack pointer
+
+ Once a function call ends we just move back up, and everything below is + available as free memory once more. +
+
+
+ + + +--- + +# Stack limitations + +The stack has limitations though, because it only grows as a result of a +function call. + +* Size of items on stack frame must be known at compile time +* If I don't know the size of a variable up front: What size should my stack +frame be? +* How can I handle arbitrary user input efficiently? + + + + + +--- + +# The Heap + +If the lifetime of some data needs to outlive a certain scope, it can not be placed on the stack. +We need another construct: the heap. + +It's all in the name, the heap is just one big pile of memory for you to store +stuff in. But what part of the heap is in use? What part is available? + +* Data comes in all shapes and sizes +* When a new piece of data comes in we need to find a place in the heap that +still has a large enough chunk of data available +* When is a piece of heap memory no longer needed? +* Where does it start? Where does it end? +* When can we start using it? + + + +--- + +# Variable scoping (recap) + +```rust +fn main() { // nothing in scope here + let i = 10; // i is now in scope + if i > 5 { + let j = i; // j is now also in scope + println!("i = {}, j = {}", i, j); + } // j is no longer in scope, i still remains + println!("i = {}", i); +} // i is no longer in scope +``` + + + +* `i` and `j` are examples containing a `Copy` type +* What if copying is too expensive? + + + + + +--- +layout: four-square +--- + +# Ownership + +::topleft:: + +```rust +let x = 5; +let y = x; +println!("{}", x); +``` + +::topright:: + +
+ + + +```text +Compiling playground v0.0.1 (/playground) +Finished dev [unoptimized + debuginfo] target(s) in 4.00s +Running `target/debug/playground` +5 +``` + + + +
+ +::bottomleft:: + + + +```rust +// Create an owned, heap allocated string +let s1 = String::from("hello"); +let s2 = s1; +println!("{}, world!", s1); +``` + + + + + +Strings store their data on the heap because they can grow + + + +::bottomright:: + + + +
+ +```text +Compiling playground v0.0.1 (/playground) +error[E0382]: borrow of moved value: `s1` +--> src/main.rs:4:28 + | +2 | let s1 = String::from("hello"); + | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait +3 | let s2 = s1; + | -- value moved here +4 | println!("{}, world!", s1); + | ^^ value borrowed here after move +``` + +
+ +
+ + + +--- + + + + + + +# Ownership + +- There is always ever only one owner of a stack value +- Once the owner goes out of scope (and is removed from the stack), any associated values on the + heap will be cleaned up as well +- Rust transfers ownership for non-copy types: *move semantics* + + + +--- + +```rust +fn main() { + let s1 = String::from("hello"); + let len = calculate_length(s1); + println!("The length of '{}' is {}.", s1, len); +} + +fn calculate_length(s: String) -> usize { + s.len() +} +``` + + + +
+ +```text +Compiling playground v0.0.1 (/playground) +error[E0382]: borrow of moved value: `s1` +--> src/main.rs:4:43 + | +2 | let s1 = String::from("hello"); + | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait +3 | let len = calculate_length(s1); + | -- value moved here +4 | println!("The length of '{}' is {}.", s1, len); + | ^^ value borrowed here after move +``` + +
+ +
+ + + +--- + +# Moving out of a function + +We can return a value to move it out of the function + +```rust +fn main() { + let s1 = String::from("hello"); + let (len, s1) = calculate_length(s1); + println!("The length of '{}' is {}.", s1, len); +} + +fn calculate_length(s: String) -> (usize, String) { + (s.len(), s) +} +``` + + + +
+ +```text +Compiling playground v0.0.1 (/playground) +Finished dev [unoptimized + debuginfo] target(s) in 5.42s +Running `target/debug/playground` +The length of 'hello' is 5. +``` + +
+ +
+ + + +--- + +# Clone + + + +- Many types in Rust are `Clone`-able +- Use can use clone to create an explicit clone (in contrast to `Copy` which + creates an implicit copy). +- Creating a clone can be expensive and could take a long time, so be careful +- Not very efficient if a clone is short-lived like in this example + +```rust +fn main() { + let x = String::from("hellothisisaverylongstring..."); + let len = get_length(x.clone()); + println!("{}: {}", x, len); +} + +fn get_length(arg: String) -> usize { + arg.len() +} +``` + + \ No newline at end of file diff --git a/slides/A-foundations/optionals-errors.md b/slides/A-foundations/optionals-errors.md new file mode 100644 index 00000000..56b1b3dd --- /dev/null +++ b/slides/A-foundations/optionals-errors.md @@ -0,0 +1,277 @@ +--- + +# Option +A quick look into the basic enums available in the standard library + +* Rust does not have null, but you can still define variables that optionally + do not have a value +* For this you can use the `Option` enum + +```rust +enum Option { + Some(T), + None, +} + +fn main() { + let some_int = Option::Some(42); + let no_string: Option = Option::None; +} +``` + + + +--- + +# Option +A quick look into the basic enums available in the standard library + +* Rust does not have null, but you can still define variables that optionally + do not have a value +* For this you can use the `Option` enum + +```rust +enum Option { + Some(T), + None, +} + +fn main() { + let some_int = Some(42); + let no_string: Option = None; +} +``` + +--- + +# Error handling +What would we do when there is an error? + +```rust +fn divide(x: i64, y: i64) -> i64 { + if y == 0 { + // what to do now? + } else { + x / y + } +} +``` + +--- + +# Error handling +What would we do when there is an error? + +```rust +fn divide(x: i64, y: i64) -> i64 { + if y == 0 { + panic!("Cannot divide by zero"); + } else { + x / y + } +} +``` + +* A panic in Rust is the most basic way to handle errors +* A panic error is an all or nothing kind of error +* A panic will immediately stop running the current thread/program and instead + immediately work to shut it down, using one of two methods: + * Unwinding: going up throught the stack and making sure that each value + is cleaned up + * Aborting: ignore everything and immediately exit the thread/program +* Only use panic in small programs if normal error handling would also exit + the program +* Avoid using panic in library code or other reusable components + + + +--- + +# Error handling +What would we do when there is an error? We could try and use the option enum +instead of panicking + +```rust +fn divide(x: i64, y: i64) -> Option { + if y == 0 { + None + } else { + Some(x / y) + } +} +``` + +--- + +# Result +Another really powerful enum is the result, which is even more useful if we +think about error handling + +```rust +enum Result { + Ok(T), + Err(E), +} + +enum DivideError { + DivisionByZero, + CannotDivideOne, +} + +fn divide(x: i64, y: i64) -> Result { + if x == 1 { + Err(DivideError::CannotDivideOne) + } else if y == 0 { + Err(DivideError::DivisionByZero) + } else { + Ok(x / y) + } +} +``` + +--- + +# Handling results +Now that we have a function that returns a result we have to think about how +we handle that error at the call-site + +```rust +fn div_zero_fails() { + match divide(10, 0) { + Ok(div) => println!("{}", div), + Err(e) => panic!("Could not divide by zero"), + } +} +``` + +* We made the signature of the `divide` function explicit in how it can fail +* The user of the function can now decide what to do, even if it is panicking +* Note: just as with `Option` we never have to use `Result::Ok` and + `Result::Err` because they have been made available globally + + + + +--- + +# Handling results +Especially when writing initial prototyping code you will often find yourself +wanting to write error handling code later, Rust has a useful utility function +to help you for both `Option` and `Result`: + +```rust +fn div_zero_fails() { + let div = divide(10, 0).unwrap(); + println!("{}", div); +} +``` + +* Unwrap checks if the Result/Option is `Ok(x)` or `Some(x)` respectively and + then return that `x`, otherwise it will panic your program with an error + message +* Having unwraps all over the place is generally considered a bad practice +* Sometimes you can ensure that an error won't occur, in such cases `unwrap` + can be a good solution + +--- + +# Handling results +Especially when writing initial prototyping code you will often find yourself +wanting to write error handling code later, Rust has a useful utility function +to help you for both `Option` and `Result`: + +```rust +fn div_zero_fails() { + let div = divide(10, 0).unwrap_or(-1); + println!("{}", div); +} +``` + +Besides unwrap, there are some other useful utility functions + +- `unwrap_or(val)`: If there is an error, use the value given to unwrap_or + instead +- `unwrap_or_default()`: Use the default value for that type if there is an + error +- `expect(msg)`: Same as unwrap, but instead pass a custom error message +- `unwrap_or_else(fn)`: Same as unwrap_or, but instead call a function that + generates a value in case of an error + + + +--- + +# Result and the `?` operator +Results are so common that there is a special operator associated with them, the +`?` operator + +```rust +fn can_fail() -> Result { + let intermediate_result = match divide(10, 0) { + Ok(ir) => ir, + Err(e) => return Err(e); + }; + + match divide(intermediate_result, 0) { + Ok(sec) => Ok(sec * 2), + Err(e) => Err(e), + } +} +``` + + + +Look how this function changes if we use the `?` operator + +```rust +fn can_fail() -> Result { + let intermediate_result = divide(10, 0)?; + Ok(divide(intermediate_result, 0)? * 2) +} +``` + + + +--- + +# Result and the `?` operator + +```rust +fn can_fail() -> Result { + let intermediate_result = divide(10, 0)?; + Ok(divide(intermediate_result, 0)? * 2) +} +``` + +* The `?` operator does an implicit match, if there is an error, that error + is then immediately returned and the function returns early +* If the result is `Ok()` then the value is extracted and we can continue right + away + diff --git a/slides/A-foundations/ownership-borrowing.md b/slides/A-foundations/ownership-borrowing.md new file mode 100644 index 00000000..0d40a278 --- /dev/null +++ b/slides/A-foundations/ownership-borrowing.md @@ -0,0 +1,292 @@ +--- + +# Ownership +We previously talked about ownership + +* In Rust there is always a single owner for each stack value +* Once the owner goes out of scope any associated values should be cleaned up +* Copy types creates copies, all other types are *moved* + + + +--- + +# Moving out of a function +We have previously seen this example + + +```rust +fn main() { + let s1 = String::from("hello"); + let len = calculate_length(s1); + println!("The length of '{}' is {}.", s1, len); +} +fn calculate_length(s: String) -> usize { + s.len() +} +``` + +* This does not compile because ownership of `s1` is moved into + `calculate_length`, meaning it is no longer available in `main` afterwards +* We can use `Clone` to create an explicit copy +* We can give ownership back by returning the value +* What about other options? + +--- + +# Borrowing +- We can make an analogy with real life: if somebody owns something you can + borrow it from them, but eventually you have to give it back +- If a value is borrowed, it is not moved and the ownership stays with the + original owner +- To borrow in Rust, we create a *reference* + +```rust {all|3|7|all} +fn main() { + let x = String::from("hello"); + let len = get_length(&x); + println!("{}: {}", x, len); +} + +fn get_length(arg: &String) -> usize { + arg.len() +} +``` + +--- + +# References (immutable) + +```rust +fn main() { + let s = String::from("hello"); + change(&s); + println!("{}", s); +} + +fn change(some_string: &String) { + some_string.push_str(", world"); +} +``` + + + +
+ +```text + Compiling playground v0.0.1 (/playground) +error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference + --> src/main.rs:8:5 + | +7 | fn change(some_string: &String) { + | ------- help: consider changing this to be a mutable reference: `&mut String` +8 | some_string.push_str(", world"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable + +For more information about this error, try `rustc --explain E0596`. +error: could not compile `playground` due to previous error +``` + +
+ +
+ + + +--- + +# References (mutable) + +```rust +fn main() { + let mut s = String::from("hello"); + change(&mut s); + println!("{}", s); +} + +fn change(some_string: &mut String) { + some_string.push_str(", world"); +} +``` + + + +
+ +```text + Compiling playground v0.0.1 (/playground) + Finished dev [unoptimized + debuginfo] target(s) in 2.55s + Running `target/debug/playground` +hello, world +``` + +
+ +
+ + + +- A mutable reference can even fully replace the original value +- To do this, you can use the dereference operator (`*`) to modify the value: + +```rust +*some_string = String::from("Goodbye"); +``` + + + + + +--- + + +# Rules for borrowing and references + +- You may only ever have one mutable reference at the same time +- You may have any number of immutable references at the same time as long as + there is no mutable reference +- References cannot *live* longer than their owners +- A reference will always at all times point to a valid value + +These rules are enforced by the Rust compiler. + + + +--- + +# Borrowing and memory safety +Combined with the ownership model we can be sure that whole classes of errors +cannot occur. + +* Rust is memory safe without having to use any runtime background process such + as a garbage collector +* But we still get the performance of a language that would normally let you + manage memory manually + + + +--- + +# Reference example + +```rust +fn main() { + let mut s = String::from("hello"); + let s1 = &s; + let s2 = &s; + let s3 = &mut s; + println!("{} - {} - {}", s1, s2, s3); +} +``` + + + +
+ +```text + Compiling playground v0.0.1 (/playground) +error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable + --> src/main.rs:5:14 + | +3 | let s1 = &s; + | -- immutable borrow occurs here +4 | let s2 = &s; +5 | let s3 = &mut s; + | ^^^^^^ mutable borrow occurs here +6 | println!("{} - {} - {}", s1, s2, s3); + | -- immutable borrow later used here + +For more information about this error, try `rustc --explain E0502`. +error: could not compile `playground` due to previous error +``` + +
+ +
+ +--- + +# Returning references + +You can return references, but the value borrowed from must exist at least as +long + +```rust +fn give_me_a_ref() -> &String { + let s = String::from("Hello, world!"); + &s +} +``` + + + +
+ +```md {8} + Compiling playground v0.0.1 (/playground) +error[E0106]: missing lifetime specifier + --> src/lib.rs:1:23 + | +1 | fn give_me_a_ref() -> &String { + | ^ expected named lifetime parameter + | + = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from +help: consider using the `'static` lifetime + | +1 | fn give_me_a_ref() -> &'static String { + | ~~~~~~~~ + +For more information about this error, try `rustc --explain E0106`. +error: could not compile `playground` due to previous error +``` + +
+ +
+ +--- + +# Returning references + +You can return references, but the value borrowed from must exist at least as +long + +```rust +fn give_me_a_ref(input: &(String, i32)) -> &String { + &input.0 +} +``` + + + +```rust +fn give_me_a_value() -> String { + let s = String::from("Hello, world!"); + s +} +``` + + diff --git a/slides/A-foundations/pattern-matching.md b/slides/A-foundations/pattern-matching.md new file mode 100644 index 00000000..434669a9 --- /dev/null +++ b/slides/A-foundations/pattern-matching.md @@ -0,0 +1,100 @@ +--- + +# Pattern matching +To extract data from enums we can use pattern matching using the +`if let [pattern] = [value]` statement + +```rust +fn accept_ipv4(ip: IpAddress) { + if let IpAddress::Ipv4(a, b, _, _) = ip { + println!("Accepted, first octet is {} and second is {}", a, b); + } +} +``` + +* `a` and `b` introduce local variables within the body of the if that contain + the values of those fields +* The underscore (`_`) can be used to accept any value + +--- + +# Match +Pattern matching is very powerful if combined with the match statement + +```rust +fn accept_home(ip: IpAddress) { + match ip { + IpAddress::Ipv4(127, 0, 0, 1) => { + println!("You are home!"); + }, + IpAddress::Ipv6(0, 0, 0, 0, 0, 0, 0, 1) => { + println!("You are in your new home!"); + }, + _ => { + println!("You are not home"); + }, + } +} +``` + +* Every part of the match is called an arm +* A match is exhaustive, which means that all values must be handled by one of + the match arms +* You can use a catch-all `_` arm to catch any remaining cases if there are any + left + +--- + +# Match as an expression +The match statement can even be used as an expression + +```rust +fn get_first_byte(ip: IpAddress) { + let first_byte = match ip { + IpAddress::Ipv4(a, _, _, _) => a, + IpAddress::Ipv6(a, _, _, _, _, _, _, _) => a / 256 as u8, + }; + println!("The first byte was: {}", first_byte); +} +``` + +* The match arms can return a value, but their types have to match +* Note how here we do not need a catch all `_` arm because all cases have + already been handled by the two arms + +--- + +# Generics +Structs become even more powerful if we introduce a little of generics + +```rust +struct PointFloat(f64, f64); +struct PointInt(i64, i64); +``` + +We are repeating ourselves here, what if we could write a data structure for +both of these cases? + + + +```rust +struct Point(T, T); + +fn main() { + let float_point: Point = Point(10.0, 10.0); + let int_point: Point = Point(10, 10); +} +``` + +Generics are much more powerful, but this is all we need for now + + + + diff --git a/slides/A-foundations/slices.md b/slides/A-foundations/slices.md new file mode 100644 index 00000000..ee32e3af --- /dev/null +++ b/slides/A-foundations/slices.md @@ -0,0 +1,361 @@ +--- + +# Vectors and arrays +What if we wanted to write a sum function, we could define one for arrays of +a specific size: + +```rust +fn sum(data: &[i64; 10]) -> i64 { + let mut total = 0; + for val in data { + total += val; + } + total +} +``` + +--- + +# Vectors and arrays +Or one for just vectors: + +```rust +fn sum(data: &Vec) -> i64 { + let mut total = 0; + for val in data { + total += val; + } + total +} +``` + +--- + +# Slices +What if we want something to work on arrays of any size? Or what if we want +to support summing up only parts of a vector? + +* A slice is a dynamically sized view into a contiguous sequence +* Contiguous: elements are layed out in memory such that they are evenly spaced +* Dynamically sized: the size of the slice is not stored in the type, but is + determined at runtime +* View: a slice is never an owned data structure +* Slices are typed as `[T]`, where `T` is the type of the elements in the slice + +--- + +# Slices + +```rust +fn sum(data: [i64]) -> i64 { + let mut total = 0; + for val in data { + total += val; + } + total +} + +fn main() { + let data = vec![10, 11, 12, 13, 14]; + println!("{}", sum(data)); +} +``` + + + +```text + Compiling playground v0.0.1 (/playground) +error[E0277]: the size for values of type `[i64]` cannot be known at compilation time + --> src/main.rs:1:8 + | +1 | fn sum(data: [i64]) -> i64 { + | ^^^^ doesn't have a size known at compile-time + | + = help: the trait `Sized` is not implemented for `[i64]` +help: function arguments must have a statically known size, borrowed types always have a known size +``` + + + + + +--- + +# Slices + +```rust +fn sum(data: &[i64]) -> i64 { + let mut total = 0; + for val in data { + total += val; + } + total +} + +fn main() { + let data = vec![10, 11, 12, 13, 14]; + println!("{}", sum(&data)); +} +``` + + + +```text + Compiling playground v0.0.1 (/playground) + Finished dev [unoptimized + debuginfo] target(s) in 0.89s + Running `target/debug/playground` +60 +``` + + + +--- + +# Slices + +* `[T]` is an incomplete type: we need to know how many `T` there are +* Types that have a known compile time size implement the `Sized` trait, raw + slices do **not** implement it +* Slices must always be behind a reference type, i.e. `&[T]` and `&mut [T]` + (but also `Box<[T]>` etc) +* The length of the slice is always stored together with the reference + +
+ + + + + + +
+ +--- + +# Creating slices +Because we cannot create slices out of thin air, they have to be located +somewhere. There are three possible ways to create slices: + +* Using a borrow + - We can borrow from arrays and vectors to create a slice of their entire + contents +* Using ranges + - We can use ranges to create a slice from parts of a vector or array +* Using a literal (for immutable slices only) + - We can have memory statically available from our compiled binary + +--- + +# Creating slices +Using a borrow + +```rust +fn sum(data: &[i32]) -> i32 { /* ... */ } + +fn main() { + let v = vec![1, 2, 3, 4, 5, 6]; + let total = sum(&v); + println!("{}", total); +} +``` + +--- + +# Creating slices +Using ranges + +```rust +fn sum(data: &[i32]) -> i32 { /* ... */ } + +fn main() { + let v = vec![0, 1, 2, 3, 4, 5, 6]; + let all = sum(&v[..]); + let except_first = sum(&v[1..]); + let except_last = sum(&v[..5]); + let except_ends = sum(&v[1..5]); +} +``` + +* The range `start..end` contains all values `x` with `start <= x < end`. + + + +* Note: you can also use ranges on their own, for example in a for loop: + +```rust +fn main() { + for i in 0..10 { + println!("{}", i); + } +} +``` + + + +--- + +# Creating slices +From a literal + +```rust {3-5,12|7-9,13|all} +fn sum(data: &[i32]) -> i32 { /* ... */ } + +fn get_v_arr() -> &'static [i32] { + &[0, 1, 2, 3, 4, 5, 6] +} + +fn get_v_vec() -> &'static [i32] { + &vec![0, 1, 2, 3, 4, 5, 6] +} + +fn main() { + let all = sum(get_v_arr()); + let all_vec = sum(get_v_vec()); +} +``` + + + +* Interestingly `get_v_arr` works, even though the literal looks like it would + only exist temporarily +* Literals actually exist during the entire lifetime of the program +* `&'static` here is used to indicate that this slice will exist the entire + lifetime of the program + + +--- + +# Strings +We have already seen the `String` type being used before, but let's dive a +little deeper + +* Strings are used to represent text +* In Rust they are always valid UTF-8 +* Their data is stored on the heap +* A String is almost the same as `Vec` with extra checks to prevent + creating invalid text + + +--- + +# Strings +Let's take a look at some strings + +```rust +fn main() { + let s = String::from("Hello world\nSee you!"); + println!("{:?}", s.split_once(" ")); + println!("{}", s.len()); + println!("{:?}", s.starts_with("Hello")); + println!("{}", s.to_uppercase()); + for line in s.lines() { + println!("{}", line); + } +} +``` + +--- + +# String literals +We have already seen string literals being used while constructing a string. +The string literal is what arrays are to vectors + +```rust +fn main() { + let s1 = "Hello world"; + let s2 = String::from("Hello world"); +} +``` + +--- + +# String literals +We have already seen string literals being used while constructing a string. +The string literal is what arrays are to vectors + +```rust +fn main() { + let s1: &'static str = "Hello world"; + let s2: String = String::from("Hello world"); +} +``` + +* `s1` is actually a slice, a string slice + +--- + +# String literals +We have already seen string literals being used while constructing a string. +The string literal is what arrays are to vectors + +```rust +fn main() { + let s1: &str = "Hello world"; + let s2: String = String::from("Hello world"); +} +``` + +* `s1` is actually a slice, a string slice + +--- + +# str - the string slice +It should be possible to have a reference to part of a string. But what is it? + +* Not `[u8]`: not every sequence of bytes is valid UTF-8 +* Not `[char]`: we could not create a slice from a string since it is stored as + UTF-8 encoded bytes +* We introduce a new special kind of slice: `str` +* For string slices we do not use brackets! + +--- + +# str, String, array, Vec + +| Static | Dynamic | Borrowed | +|----------|----------|----------| +| `[T; N]` | `Vec` | `&[T]` | +| - | `String` | `&str` | + +* There is no static variant of str +* This would only be useful if we wanted strings of an exact length +* But just like we had the static slice literals, we can use `&'static str` + literals for that instead! + +--- + +# String or str +When do we use String and when do we use str? + +```rust +fn string_len(data: &String) -> usize { + data.len() +} +``` + +--- + +# String or str +When do we use String and when do we use str? + +```rust +fn string_len(data: &str) -> usize { + data.len() +} +``` + +* Prefer `&str` over `String` whenever possible +* If you need to mutate a string you might try `&mut str`, but you cannot + change a slice's length +* Use `String` or `&mut String` if you need to fully mutate the string diff --git a/slides/A-foundations/smart-pointers.md b/slides/A-foundations/smart-pointers.md new file mode 100644 index 00000000..2767530d --- /dev/null +++ b/slides/A-foundations/smart-pointers.md @@ -0,0 +1,41 @@ +--- + +# Boxing +There are several reasons to box a variable on the heap + +* When something is too large to move around +* We need something that is sized dynamically +* For writing recursive data structures + +```rust +struct Node { + data: Vec, + parent: Node, +} +``` + +--- + +# Boxing +There are several reasons to box a variable on the heap + +* When something is too large to move around +* We need something that is sized dynamically +* For writing recursive data structures + +```rust +struct Node { + data: Vec, + parent: Box, +} +``` + + diff --git a/slides/A-foundations/trait-objects.md b/slides/A-foundations/trait-objects.md new file mode 100644 index 00000000..eb9c8a2c --- /dev/null +++ b/slides/A-foundations/trait-objects.md @@ -0,0 +1,338 @@ +--- +layout: section +--- +# Trait objects & dynamic dispatch + +--- +layout: default +--- + +# Trait... Object? +- We learned about traits in module A3 +- We learned about generics and `monomorphization` + +There's more to this story though... + +*Question: What was monomorphization again?* + +--- +layout: default +--- + +# Monomorphization: recap + +```rust +impl MyAdd for i32 {/* - snip - */} +impl MyAdd for f32 {/* - snip - */} + +fn add_values(left: &T, right: &T) -> T +{ + left.my_add(right) +} + +fn main() { + let sum_one = add_values(&6, &8); + assert_eq!(sum_one, 14); + let sum_two = add_values(&6.5, &7.5); + println!("Sum two: {}", sum_two); // 14 +} +``` + +Code is monomorphized: + - Two versions of `add_values` end up in binary + - Optimized separately and very fast to run (static dispatch) + - Slow to compile and larger binary + +--- +layout: default +--- + +# Dynamic dispatch +*What if don't know the concrete type implementing the trait at compile time?* + +```rust{all|1-8|10-12|14-23|17-20} +use std::io::Write; +use std::path::PathBuf; + +struct FileLogger { log_path: PathBuf } +impl Write for FileLogger { /* - snip -*/} + +struct StdOutLogger; +impl Write for StdOutLogger { /* - snip -*/} + +fn log(entry: &str, logger: &mut L) { + write!(logger, "{}", entry); +} + +fn main() { + let log_file: Option = + todo!("read args"); + let mut logger = match log_file { + Some(log_path) => FileLogger { log_path }, + Nome => StdOutLogger, + }; + + log("Hello, world!🦀", &mut logger); +} +``` + +--- +layout: default +--- +# Error! + + +```txt +error[E0308]: `match` arms have incompatible types + --> src/main.rs:19:17 + | +17 | let mut logger = match log_file { + | ______________________- +18 | | Some(log_path) => FileLogger { log_path }, + | | ----------------------- this is found to be of type `FileLogger` +19 | | Nome => StdOutLogger, + | | ^^^^^^^^^^^^ expected struct `FileLogger`, found struct `StdOutLogger` +20 | | }; + | |_____- `match` arms have incompatible types +``` + +*What's the type of `logger`?* + +--- +layout: default +--- + +# Heterogeneous collections +*What if we want to create collections of different types implementing the same trait?* + +```rust{all|1-13|15-21} +trait Render { + fn paint(&self); +} + +struct Circle; +impl Render for Circle { + fn paint(&self) { /* - snip - */ } +} + +struct Rectangle; +impl Render for Rectangle { + fn paint(&self) { /* - snip - */ } +} + +fn main() { + let mut shapes = Vec::new(); + let circle = Circle; + shapes.push(circle); + let rect = Rectangle; + shapes.push(rect); + shapes.iter().for_each(|shape| shape.paint()); +} +``` + +--- +layout: default +--- + +# Error again! +```txt + Compiling playground v0.0.1 (/playground) +error[E0308]: mismatched types + --> src/main.rs:20:17 + | +20 | shapes.push(rect); + | ---- ^^^^ expected struct `Circle`, found struct `Rectangle` + | | + | arguments to this method are incorrect + | +note: associated function defined here + --> /rustc/2c8cc343237b8f7d5a3c3703e3a87f2eb2c54a74/library/alloc/src/vec/mod.rs:1836:12 + +For more information about this error, try `rustc --explain E0308`. +error: could not compile `playground` due to previous error +``` + +*What is the type of `shapes`?* + +--- +layout: default +--- +# Trait objects to the rescue + +- Opaque type that implements a set of traits +- Type description: `dyn T: !Sized` where `T` is a `trait` +- Like slices, Trait Objects always live behind pointers (`&dyn T`, `&mut dyn T`, `Box`, `...`) +- Concrete underlying types are erased from trait object + +```rust{all|5-7} +fn main() { + let log_file: Option = + todo!("read args"); + // Create a trait object that implements `Write` + let logger: &mut dyn Write = match log_file { + Some(log_path) => &mut FileLogger { log_path }, + Nome => &mut StdOutLogger, + }; +} +``` +--- +layout: two-cols +--- + +# Layout of trait objects + +```rust +/// Same code as last slide +fn main() { + let log_file: Option = + todo!("read args"); + // Create a trait object that implements `Write` + let logger: &mut dyn Write = match log_file { + Some(log_path) => &mut FileLogger { log_path }, + Nome => &mut StdOutLogger, + }; + + log("Hello, world!🦀", &mut logger); +} +``` + + +- *💸 Cost: pointer indirection via vtable → less performant* +- *💰 Benefit: no monomorphization → smaller binary & shorter compile time!* + + +::right:: + + + + +--- +layout: default +--- + +# Fixing dynamic logger + +- Trait objects `&dyn T`, `Box`, ... implement `T`! + +```rust{all|9-12|1-2} +// We no longer require L be `Sized`, so to accept trait objects +fn log(entry: &str, logger: &mut L) { + write!(logger, "{}", entry); +} + +fn main() { + let log_file: Option = + todo!("read args"); + // Create a trait object that implements `Write` + let logger: &mut dyn Write = match log_file { + Some(log_path) => &mut FileLogger { log_path }, + Nome => &mut StdOutLogger, + }; + + log("Hello, world!🦀", logger); +} +``` +And all is well! + +--- +layout: default +--- + +# Forcing dynamic dispatch + +Sometimes you want to enforce API users (or colleagues) to use dynamic dispatch + +```rust{all|1} +fn log(entry: &str, logger: &mut dyn Write) { + write!(logger, "{}", entry); +} + +fn main() { + let log_file: Option = + todo!("read args"); + // Create a trait object that implements `Write` + let logger: &mut dyn Write = match log_file { + Some(log_path) => &mut FileLogger { log_path }, + Nome => &mut StdOutLogger, + }; + + + log("Hello, world!🦀", &mut logger); +} +``` + +--- +layout: default +--- + +# Fixing the renderer + +```rust +fn main() { + let mut shapes = Vec::new(); + let circle = Circle; + shapes.push(circle); + let rect = Rectangle; + shapes.push(rect); + shapes.iter().for_each(|shape| shape.paint()); +} +``` + +Becomes + +```rust{all|2,3,5} +fn main() { + let mut shapes: Vec> = Vec::new(); + let circle = Box::new(Circle); + shapes.push(circle); + let rect = Box::new(Rectangle); + shapes.push(rect); + shapes.iter().for_each(|shape| shape.paint()); +} +``` + +All set! + + +--- +layout: default +--- + +# Trait object limitations + +- Pointer indirection cost +- Harder to debug +- Type erasure +- Not all traits work: + +*Traits need to be 'Object Safe'* + + +--- +layout: default +--- + +# Object safety + +In order for a trait to be object safe, these conditions need to be met: + +- If `trait T: Y`, then`Y` must be object safe +- trait `T` must not be `Sized`: *Why?* +- No associated constants allowed* +- No associated types with generic allowed* +- All associated functions must either be dispatchable from a trait object, or explicitly non-dispatchable + - e.g. function must have a receiver with a reference to `Self` + +Details in [The Rust Reference](https://doc.rust-lang.org/reference/items/traits.html#object-safety). Read them! + +*These seem to be compiler limitations + +--- +layout: default +--- + +# So far... + +- Trait objects allow for dynamic dispatch and heterogeneous +- Trait objects introduce pointer indirection +- Traits need to be object safe to make trait objects out of them \ No newline at end of file diff --git a/slides/A-foundations/traits-generics.md b/slides/A-foundations/traits-generics.md new file mode 100644 index 00000000..8bb1709e --- /dev/null +++ b/slides/A-foundations/traits-generics.md @@ -0,0 +1,336 @@ +--- +layout: section +--- +# Introduction to generics + +--- +layout: default +--- +# The problem + +```rust +fn add_u32(l: u32, r: u32) -> u32 { /* -snip- */ } + +fn add_i32(l: i32, r: i32) -> i32 { /* -snip- */ } + +fn add_f32(l: f32, r: f32) -> f32 { /* -snip- */ } + +/* ... */ +``` + + +
+We need generic code! +
+
+ + + +--- +layout: default +--- +# Generic code + +An example +```rust +fn add(lhs: T, rhs: T) -> T { /* - snip - */} +``` + + +
+
+Or, in plain English: + +- `` = "let `T` be a type" +- `lhs: T` "let `lhs` be of type `T`" +- `-> T` "let `T` be the return type of this function" +
+
+ +
+
+Some open points: + +- What can we do with a `T`? +- What should the body be? +
+
+ +--- +layout: default +--- +# Bounds on generic code +  + +We need to provide information to the compiler: +- Tell Rust what `T` can do +- Tell Rust what `T` is accepted +- Tell Rust how `T` implements functionality + +--- +layout: default +--- + +# `trait` +  + +Describe what the type can do +```rust +trait MyAdd { + fn my_add(&self, other: &Self) -> Self; +} +``` + +--- +layout: default +--- +# `impl trait` +  + +Describe how the type does it + +```rust{all|1|2-8} +impl MyAdd for u32 { + fn my_add(&self, other: &Self) -> Self { + *self + *other + } +} +``` + +--- +layout: default +--- +# Using a `trait` + +```rust{all|1-2|5-6|7-9|10-12} +// Import the trait +use my_mod::MyAdd + +fn main() { + let left: u32 = 6; + let right: u32 = 8; + // Call trait method + let result = left.my_add(&right); + assert_eq!(result, 14); + // Explicit call + let result = MyAdd::my_add(&left, &right); + assert_eq!(result, 14); +} +``` + +- Trait needs to be in scope +- Call just like a method +- Or by using the explicit associated function syntax + +--- +layout: default +--- +# Trait bounds + +```rust{all|1-3,5|5,7-11} +fn add_values(this: &T, other: &T) -> T { + this.my_add(other) +} + +// Or, equivalently + +fn add_values(this: &T, other: &T) -> T + where T: MyAdd +{ + this.my_add(other) +} +``` + +Now we've got a *useful* generic function! + +English: *"For all types `T` that implement the `MyAdd` `trait`, we define..."* + +--- +layout: default +--- +# Limitations of `MyAdd` +What happens if... + +- We want to add two values of different types? +- Addition yields a different type? + +--- +layout: default +--- + +# Making `MyAdd` itself generic +  + +Add an 'Input type' `O`: + +```rust{all|1-3|5-9} +trait MyAdd { + fn my_add(&self, other: &O) -> Self; +} + +impl MyAdd for u32 { + fn my_add(&self, other: &u16) -> Self { + *self + (*other as u32) + } +} +``` + +We can now add a `u16` to a `u32`. + +--- +layout: default +--- + +# Defining output of `MyAdd` + +- Addition of two given types always yields in one specific type of output +- Add *associated type* for addition output + +```rust{all|2-3|7-9|6-20} +trait MyAdd { + type Output; + fn my_add(&self, other: &O) -> Self::Output; +} + +impl MyAdd for u32 { + type Output = u64; + + fn my_add(&self, other: &u16) -> Self::Output { + *self as u64 + (*other as u64) + } +} + +impl MyAdd for u32 { + type Output = u32; + + fn my_add(&self, other: &u32) -> Self::Output { + *self + *other + } +} +``` + +--- +layout: default +--- +# `std::ops::Add` +The way `std` does it + +```rust{all|1|2-4} +pub trait Add { + type Output; + + fn add(self, rhs: Rhs) -> Self::Output; +} +``` + +- Default type of `Self` for `Rhs` + +--- +layout: default +--- +# `impl std::ops::Add` + +```rust +use std::ops::Add; +pub struct BigNumber(u64); + +impl Add for BigNumber { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + BigNumber(self.0 + rhs.0) + } +} + +fn main() { + // Call `Add::add` + let res = BigNumber(1).add(BigNumber(2)); +} +``` + +What's the type of `res`? + +--- +layout: default +--- +# `impl std::ops::Add` (2) + +```rust +pub struct BigNumber(u64); + +impl std::ops::Add for BigNumber { + type Output = u128; + + fn add(self, rhs: Self) -> Self::Output { + (self.0 as u128) + (rhs as u128) + } +} + +fn main() { + let res = BigNumber(1) + 3u32; +} +``` + +What's the type of `res`? + +--- +layout: default +--- +# Traits: Type Parameter vs. Associated Type + +### Type parameter (input type) +*if trait can be implemented for many combinations of types* +```rust +// We can add both a u32 value and a u32 reference to a u32 +impl Add for u32 {/* */} +impl Add<&u32> for u32 {/* */} +``` + +### Associated type (output type) +*to define a type for a single implementation* +```rust +impl Add for u32 { + // Addition of two u32's is always u32 + type Output = u32; +} +``` + +--- +layout: default +--- + +# `#[derive]` a `trait` + +```rust +#[derive(Clone)] +struct Dolly { + num_legs: u32, +} + +fn main() { + let dolly = Dolly { num_legs: 4 }; + let second_dolly = dolly.clone(); + assert_eq!(dolly.num_legs, second_dolly.num_legs); +} +``` + +- Some traits are trivial to implement +- Derive to quickly implement a trait +- For `Clone`: derived `impl` calls `clone` on each field + +--- +layout: default +--- +# Orphan rule + +*Coherence: There must be **at most one** implementation of a trait for any given type* + +Trait can be implemented for a type **iff**: +- Either your crate defines the trait +- Or your crate defines the type + +Or both, of course diff --git a/slides/A-foundations/vec.md b/slides/A-foundations/vec.md new file mode 100644 index 00000000..74d92611 --- /dev/null +++ b/slides/A-foundations/vec.md @@ -0,0 +1,87 @@ +--- + +# Vec: storing more of the same +The vector is an array that can grow + +* Compare this to the array we previously saw, which has a fixed size + +```rust +fn main() { + let arr = [1, 2]; + println!("{:?}", arr); + + let mut nums = Vec::new(); + nums.push(1); + nums.push(2); + println!("{:?}", nums); +} +``` + +--- + +# Vec +Vec is such a common type that there is an easy way to initialize +it with values that looks similar to arrays + +```rust +fn main() { + let mut nums = vec![1, 2]; + nums.push(3); + println!("{:?}", nums); +} +``` + +--- + +# Vec: memory layout +How can a vector grow? Things on the stack need to be of a fixed size + +
+ + + + + + +
+ + + +--- + +# Put it in a box +That pointer from the stack to the heap, how do we create such a thing? + +* Boxing something is the way to store a value on the heap +* A `Box` uniquely owns that value, there is no one else that also owns that same + value +* Even if the type inside the box is `Copy`, the box itself is not, move + semantics apply to a box. + +```rust +fn main() { + // put an integer on the heap + let boxed_int = Box::new(10); +} +``` +
+ + + + + + +
diff --git a/slides/A-foundations/why-rust.md b/slides/A-foundations/why-rust.md new file mode 100644 index 00000000..90a7ee60 --- /dev/null +++ b/slides/A-foundations/why-rust.md @@ -0,0 +1,25 @@ +--- +layout: section +--- + +# Why learn Rust? + +## by Florian Gilcher + +Founder of Ferrous Systems + +Rust training and evangelisation + +Company support + +Tooling + +Ferrocene: Rust in automotive + +--- +layout: default +--- +# Why learn Rust? + + + diff --git a/slides/package.json b/slides/package.json index 59731093..dbc1662c 100644 --- a/slides/package.json +++ b/slides/package.json @@ -1,6 +1,8 @@ { "private": true, "scripts": { + "dev-A": "slidev A-foundations/slides.md", + "build-0": "slidev build --out dist-0 --base /slides/0/ 0-intro.md", "dev-0": "slidev 0-intro.md", "export-0": "slidev export 0-intro.md",