From e73197aa265c384cfbae4bcfdcdd24555ba8036d Mon Sep 17 00:00:00 2001 From: darkicewolf50 Date: Wed, 16 Apr 2025 15:44:05 -0600 Subject: [PATCH] finished ch20.3 --- .obsidian/workspace.json | 26 ++- Advanced Features.md | 2 +- Advanced Functions and Closures.md | 1 + Advanced Types.md | 332 +++++++++++++++++++++++++++++ 4 files changed, 354 insertions(+), 7 deletions(-) create mode 100644 Advanced Functions and Closures.md diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 675729e..2093329 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -21,6 +21,20 @@ "title": "Advanced Features" } }, + { + "id": "b24fe202609f5b35", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "Advanced Functions and Closures.md", + "mode": "source", + "source": false + }, + "icon": "lucide-file", + "title": "Advanced Functions and Closures" + } + }, { "id": "8efdb57e394f2650", "type": "leaf", @@ -74,7 +88,7 @@ } } ], - "currentTab": 2 + "currentTab": 1 } ], "direction": "vertical" @@ -217,12 +231,13 @@ "command-palette:Open command palette": false } }, - "active": "74db89a42def0b8b", + "active": "b24fe202609f5b35", "lastOpenFiles": [ - "Advanced Traits.md", - "Advanced Types.md", - "Unsafe Rust.md", "Advanced Features.md", + "Advanced Functions and Closures.md", + "Advanced Types.md", + "Advanced Traits.md", + "Unsafe Rust.md", "Pattern Matching.md", "Places Patterns Can Be Used.md", "Pattern Syntax.md", @@ -244,7 +259,6 @@ "Smart Pointers.md", "Simultaneous Code Running.md", "Passing Data Between Threads.md", - "Leaky Reference Cycles.md", "minigrep/src/lib.rs", "does_not_compile.svg", "Untitled.canvas", diff --git a/Advanced Features.md b/Advanced Features.md index a5ebeb2..0f08674 100644 --- a/Advanced Features.md +++ b/Advanced Features.md @@ -11,6 +11,6 @@ In this chapter we will cover - [Unsafe Rust](./Unsafe%20Rust.md): How to opt out of some of Rust's guarantees and take responsibility for manually upholding those guarantees - [Advanced traits](./Advanced%20Traits.md): associated types, default type parameters, fully qualified syntax, supertraits, and the new type pattern in relation to traits - [Advanced types](./Advanced%20Types.md): more about the newtype pattern, type aliases, the never type, and dynamically sized types -- [Advanced functions and closures](): function pointers and returning closures +- [Advanced functions and closures](./Advanced%20Functions%20and%20Closures.md): function pointers and returning closures - [Macros](): ways to define code that defines more code at compile time a \ No newline at end of file diff --git a/Advanced Functions and Closures.md b/Advanced Functions and Closures.md new file mode 100644 index 0000000..8451555 --- /dev/null +++ b/Advanced Functions and Closures.md @@ -0,0 +1 @@ +# Advanced Functions and Closures diff --git a/Advanced Types.md b/Advanced Types.md index dccf4da..cec2580 100644 --- a/Advanced Types.md +++ b/Advanced Types.md @@ -1 +1,333 @@ # Advanced Types +The Rust type system has some features that we have mentioned so far but haven't gone into detail. + +To start we will go into the newtypes in general as we examine why newtypes are useful as types. + +Then we will go onto type aliases, a feature similar to newtypes but slightly different semantics. + +As well we will discuss the `!` and dynamically sized types. + +## Using the Newtype Pattern for Type Safety and Abstraction +The newtype pattern are also useful for tasks beyond those discussed already. + +This includes statically enforcing that values are never confused and indicating the units of a value. + +Before we saw an example of using newtypes to indicate units: recall that the `Millimeters` and `Meters` structs wrapped `u32` values in a newtype. + +If we wrote a function with a parameter of type `Millimeters`, we couldn't compile a program that accidentally tired to call function with a value of type `Meters` or a plain `u32`. + +We can also use the newtype pattern to abstract away some implementation details of a type. + +The new type can expose a public API that is different form the API of the private inner type. + +Newtypes can also hide internal implementation. + +Lets say we could provide a `People` type to wrap a `HashMap` that store a person's ID associated with their name. + +Code using `People` would only interact with the public API we provide. + +Like a method to add a name string to the `People` collect: this code wouldn't need to know that we assign an `i32` ID to names internally. + +The newtype pattern is a lightweight way to achieve encapsulation to hide implementation details, which we discussed before in [Ch18](./Characteristics%20of%20OO%20Languages.md#encapsulation-that-hides-implementation-details). + +## Creating Type Synonyms with Type Aliases +Rust provides the ability to declare a *type alias* to give an existing type another name. + +We need to use the `type` keyword to do this. + +For example we can create the alias `Kilometers` to `i32` like this. +```rust + type Kilometers = i32; +``` +The alias `Kilometers` is a *synonym* for `i32`. + +Unlike the `Millimeters` and `Meters` types we created before. + +`Kilometers` is not a separate, new type. + +Values that have the type `Kilometers` will be treated the same as values of type `i32`. +```rust + type Kilometers = i32; + + let x: i32 = 5; + let y: Kilometers = 5; + + println!("x + y = {}", x + y); +``` +Because `Kilometers` and `i32` are the same type, we can add values of both types and we can pass `Kilometers` values to functions that take `i32` parameters. + +However using this method, we don't get the type checking benefits that we get from the newtype pattern discussed earlier. + +In other words, if we mix up `Kilometers` and `i32` values somewhere, the compiler will not give us an error. + +The main use for type synonyms is to reduce repetition. + +As an example, we might have a lengthy type like this. +```rust +Box +``` +Writing this lengthy type function signatures and as type annotations all over the code can be tiresome and error prone. + +Just image a project full of code like this. +```rust + let f: Box = Box::new(|| println!("hi")); + + fn takes_long_type(f: Box) { + // --snip-- + } + + fn returns_long_type() -> Box { + // --snip-- + } +``` +A type alias makes this code more manageable by reducing the amount of repetition. + +Here we have introduced an alias named `Thunk` for the verbose type and can replace all uses of the type with the shorter alias `Thunk`. +```rust + type Thunk = Box; + + let f: Thunk = Box::new(|| println!("hi")); + + fn takes_long_type(f: Thunk) { + // --snip-- + } + + fn returns_long_type() -> Thunk { + // --snip-- + } +``` +This is much easier to read and write. + +Choosing a meaningful name for a type alias can help communicate your intent as well. + +*Thunk* is a word for code to be evaluated at a later time, this is an appropriate name for a closure that gets stored. + +Type aliases are also commonly used with the `Result` type for repetition. + +Consider the `std::io` module in the std library. + +I/O operations often return a `Result` to handle situations when operations fail to work. + +This library has a `std::io::Error` struct that represents all possible I/O errors. + +Many of the functions in `std::io` will be returning `Result` where the `E` is `std::io::Error`, such as these functions in `Write` trait: +```rust +use std::fmt; +use std::io::Error; + +pub trait Write { + fn write(&mut self, buf: &[u8]) -> Result; + fn flush(&mut self) -> Result<(), Error>; + + fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>; + fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>; +} +``` +The `Result<..., Error>` is repeated a lot. + +Therefore `std::io` has this type alias declaration +```rust +type Result = std::result::Result; +``` +Due to this declaration is in the `std::io` module, we can use the fully qualified alias `std::io::Result`. + +That is a `Result` with the `E` filled in as `std::io::Error`. + +The `Write` trait function signatures end up looking like this. +```rust +pub trait Write { + fn write(&mut self, buf: &[u8]) -> Result; + fn flush(&mut self) -> Result<()>; + + fn write_all(&mut self, buf: &[u8]) -> Result<()>; + fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>; +} +``` +This type alias helps in two ways: +- It makes code easier to write. +- *And* +- It gives us a consistent interface across all of `std::io` +Due to it being an alias, it is just another `Result`, this means we can use any methods that work on `Result` with it, as well as special syntax like the `?` operator. + +## The Never Type that Never Returns +Rust has a special type named `!` that is known in type theory lingo as the *empty type* because it has no values. + +We prefer to call it the *never type* because it stands in the place of the return type when a function will never return. + +Here is an example in use. +```rust +fn bar() -> ! { + // --snip-- +} +``` +This code should be read as "the function `bar` returns never." + +Functions that return never are called *diverging functions*. + +We can't create values of the type `!` so `bar` can never possibly return. + +What is the use of a type you can never create values for? + +Recall the code from Ch2, part of the number guessing game. + +Here is a sample of that code +```rust + let guess: u32 = match guess.trim().parse() { + Ok(num) => num, + Err(_) => continue, + }; +``` +Before we skipped over some details about this code. + +In ch6 we discussed that `match` arms must all return the same type. + +For example this code will not compile. +```rust + let guess = match guess.trim().parse() { + Ok(_) => 5, + Err(_) => "hello", + }; +``` +The type of `guess` in this code would have to be an integer *and* a string, and Rust requires that `guess` have only one type. + +So what does `continue` return? + +How are we allowed to return a `u32` from one arm and have another arm that ends with `continue`? + +`continue` has a `!` value. + +That is, when Rust computes the type of `guess`, it looks at both match arms, the former with a value of `u32` and the latter with a `!` value. + +Because `!` can never have a value, Rust decides that the type of `guess` is `u32`. + +The formal way to describe this behavior is that expressions of type `!` can be coerced into any other type. + +We are allowed to end this `match` arm with `continue` because `continue` doesn't return a value. + +Instead it moves control back to the top of the loop, so in the `Err` case, we never assign a value to `guess`. + +The never type is useful with the `panic!` macro as well. + +Remember the `unwrap` function that we call on `Option` values to produce a value or panic with this definition: +```rust +impl Option { + pub fn unwrap(self) -> T { + match self { + Some(val) => val, + None => panic!("called `Option::unwrap()` on a `None` value"), + } + } +} +``` +Here, the same thing happens as in the `match` case form before. + +Rust sees that `val` has the type `T` and `panic!` has the type `!`, so the result of the overall `match` expression is `T`. + +This works because `panic!` doesn't produce a value, it ends the program. + +In the `None` case, we will not be returning a value form `unwarp` so this code is valid. + +One final expression that has the type `!` is a `loop`. +```rust + print!("forever "); + + loop { + print!("and ever "); + } +``` +This loop never ends, so `!` is the value of the expression. + +However, this wouldn't be true if we included a `break`, because the loop would terminate when it got to the `break`. + +## Dynamically Sized Types and the `Sized` Trait +Rust must know certain details about its types, such as how much space to allocate for a value of a particular type. + +This leaves one corner of its type system a little confusing at first: the concept of *dynamically sized types*. + +Sometimes referred to as *DSTs* or *unsized types*, these types let us write code using values whose size we can know only at runtime. + +Lets look into the details of a dynamically sized type called `str`, which we have been using throughout. + +This does not include `&str`, but `str` on its own, is a DST. + +We can't know how long the string is until runtime, meaning we can't create a variable of type `str`, nor can we make that argument of type `str`. + +Consider this code, which will not compile. +```rust + let s1: str = "Hello there!"; + let s2: str = "How's it going?"; +``` +Rust needs to know how much memory to allocate for any value of a particular type, and all values of a type must use the same amount of memory. + +If Rust allowed use to write this code, these two `str` values would need to take up the same amount of memory. + +These two have different lengths: +- `s1` needs 12 bytes of storage. +- `s2` needs 15. +This is why it is not possible to create a variable holding a dynamically sized type. + +So what should we do? + +We should make the types of `s1` and `s2` a `&str` rather than a `str`. + +Recall from the "String Slice" section from Ch4, that the slice data structure just stores the starting position and the length of the slice. + +Even though a `&T` is a single value that stores the memory address of where the `T` is located, a `&str` is *two* values. + +The address of the `str` and its length. + +We can know the size of a `&str` value at compile time: it's twice the length of a `usize`. + +This means we always know the size of a `&str`, no matter how long the string it refers to is. + +Generally this is the way in which dynamically sized types are used in Rust, they have an extra but of metadata that stores the size of the dynamic information. + +The golden rule of dynamically sized types is that we must always put values of dynamically sized types behind a pointer of some kind. + +We can combine `str` with all kinds of pointers. + +For example `Box` or `Rc`. + +In fact we have seen this before but with a different dynamically sized type: traits. + +Every trait is a dynamically sized type we can refer to by using the name of the trait. + +In Ch18 in ["Using Trait Objects That Allow for Values of Different Types"](./Characteristics%20of%20OO%20Languages.md#encapsulation-that-hides-implementation-details), we mentioned that to use trait as trait objects, we must put them behind a pointer, such as `&dyn Trait` or `Box` (`Rc` would work as well). + +To work with DSTs, Rust provides the `Sized` trait to determine whether or not a type's size is known at compile time. + +This trait is automatically implemented for everything whose size is known at compile time. + +Additionally Rust implicitly adds a bound on `Sized` to every generic function. + +That is, a generic function definition like this: +```rust +fn generic(t: T) { + // --snip-- +} +``` +This is actually treated as though we had written this: +```rust +fn generic(t: T) { + // --snip-- +} +``` +By default, generic functions will work only on types that have a known size at compile time. + +However, you can use the following special syntax to relax this restriction. +```rust +fn generic(t: &T) { + // --snip-- +} +``` +A trait bound on `?Sized` means "`T` may or may not be `Sized`". + +This notation overrides the default that generic types must have a known size at compile time. + +The `?Trait` syntax with this meaning is only available for `Sized`, not any other traits. + +Note that we switched the type of the `t` parameter from `T` to `&T`. + +Because the type might not be `Sized`, we need to use it behind some kind of pointer. + +Here we have chosen to use a reference. \ No newline at end of file