diff --git a/src/SUMMARY.md b/src/SUMMARY.md index a5929bad8455..22273d9c772a 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -470,6 +470,35 @@ - [Branded pt 2: `PhantomData` and Lifetime Subtyping](idiomatic/leveraging-the-type-system/token-types/branded-02-phantomdata.md) - [Branded pt 3: Implementation](idiomatic/leveraging-the-type-system/token-types/branded-03-impl.md) - [Branded pt 4: Branded types in action.](idiomatic/leveraging-the-type-system/token-types/branded-04-in-action.md) +- [Polymorphism](idiomatic/polymorphism.md) + - [Refresher](idiomatic/polymorphism/refresher.md) + - [Traits](idiomatic/polymorphism/refresher/01-traits.md) + - [Trait Bounds](idiomatic/polymorphism/refresher/02-trait-bounds.md) + - [Deriving Traits](idiomatic/polymorphism/refresher/03-deriving-traits.md) + - [Default Implementations](idiomatic/polymorphism/refresher/04-default-impls.md) + - [Supertraits](idiomatic/polymorphism/refresher/05-supertraits.md) + - [Blanket Implementations](idiomatic/polymorphism/refresher/06-blanket-impls.md) + - [Conditional Methods](idiomatic/polymorphism/refresher/07-conditional-methods.md) + - [Orphan Rule](idiomatic/polymorphism/refresher/08-orphan-rule.md) + - [Statically Sized and Dynamically Sized types](idiomatic/polymorphism/refresher/09-sized.md) + - [Monomophization and Binary Size](idiomatic/polymorphism/refresher/10-monomorphization.md) + - [From OOP to Rust](idiomatic/polymorphism/from-oop-to-rust.md) + - [Inheritance](idiomatic/polymorphism/from-oop-to-rust/01-inheritance.md) + - [Why no Inheritance in Rust?](idiomatic/polymorphism/from-oop-to-rust/02-why-no-inheritance.md) + - [Inheritance from Rust's Perspective](idiomatic/polymorphism/from-oop-to-rust/03-switch-perspective.md) + - ["Inheritance" in rust and Supertraits](idiomatic/polymorphism/from-oop-to-rust/04-supertraits.md) + - [Composition over Inheritance](idiomatic/polymorphism/from-oop-to-rust/05-composition.md) + - [Trait Objects and Dynamic Dispatch](idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/01-dyn-trait.md) + - [Dyn Compatibility](idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/02-dyn-compatible.md) + - [Generics vs Trait Objects](idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/03-dyn-vs-generics.md) + - [Limits of Trait Objects](idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/04-limits.md) + - [Heterogeneous Collections](idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/05-heterogeneous.md) + - [The `Any` Trait](idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/06-any-trait.md) + - [Pitfall: Reaching too quickly for `dyn Trait`](idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/07-pitfalls.md) + - [Sealed Traits](idiomatic/polymorphism/from-oop-to-rust/07-sealed-traits.md) + - [Sealing with Enums](idiomatic/polymorphism/from-oop-to-rust/08-sealing-with-enums.md) + - [Traits for Polymorphism users can extend](idiomatic/polymorphism/from-oop-to-rust/09-sticking-with-traits.md) + - [Problem solving: Break Down the Problem](idiomatic/polymorphism/from-oop-to-rust/10-problem-solving.md) --- diff --git a/src/idiomatic/polymorphism.md b/src/idiomatic/polymorphism.md new file mode 100644 index 000000000000..ed4dd37c71e6 --- /dev/null +++ b/src/idiomatic/polymorphism.md @@ -0,0 +1,30 @@ +--- +minutes: 2 +--- + +# Polymorphism + +```rust +pub trait Trait {} + +pub struct HasGeneric(T); + +pub enum Either { + Left(A), + Right(B), +} + +fn takes_generic(value: &T) {} + +fn takes_dyn(value: &dyn Trait) {} +``` + +
+ +- Rust has plenty of mechanisms for writing and using polymorphic code, but + they're quite different from other languages! + +- This chapter will cover the details of Rust's polymorphism and how it's + similar, or different to, other languages. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust.md b/src/idiomatic/polymorphism/from-oop-to-rust.md new file mode 100644 index 000000000000..99b21c795acd --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust.md @@ -0,0 +1,18 @@ +# From OOP to Rust: Composition, not Inheritance + +```rust +``` + +
+ +- Inheritance is key to OOP's success as a paradigm. Decades of successful + software engineering has been done with Inheritance as a core part of business + logic. + +- So why did rust avoid inheritance? + +- How do we move from inheritance-based problem solving to rust's approach? + +- How do I represent heterogeneous collections in rust? + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/01-inheritance.md b/src/idiomatic/polymorphism/from-oop-to-rust/01-inheritance.md new file mode 100644 index 000000000000..0bd82cf4ecc8 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/01-inheritance.md @@ -0,0 +1,40 @@ +--- +minutes: 5 +--- + +# Inheritance in OOP languages + +```cpp +#include +using namespace std; + +// Base class +class Vehicle { +public: + void accellerate() { } + void brake() { } +}; + +// Inheriting class +class Car : public Vehicle { +public: + void honk() { } +}; + +int main() { + Car myCar; // Create a Car object + myCar.accellerate(); // Inherited method + myCar.honk(); // Car's own method + myCar.brake(); // Inherited method + return 0; +} +``` + +- Here to remind students what inheritance is. + +- Inheritance is a mechanism where a "child" type gains the fields and methods + of the "parent" types it is inheriting from. + +- Methods are able to be overridden as-needed by the inheriting type. + +- Can call methods of inherited-from classes with `super`. diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/02-why-no-inheritance.md b/src/idiomatic/polymorphism/from-oop-to-rust/02-why-no-inheritance.md new file mode 100644 index 000000000000..555b2fad6838 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/02-why-no-inheritance.md @@ -0,0 +1,72 @@ +--- +minutes: 10 +--- + +# Why no inheritance in Rust? + +```rust,compile_fail +pub struct Id { + pub id: u32 +} + +impl Id { + // methods +} + +// 🔨❌, rust does not have inheritance! +pub struct Data: Id { + // Inherited "id" field + pub name: String, +} + +impl Data { + // methods, but also includes Id's methods, or maybe overrides to + // those methods. +} + +// ✅ +pub struct Data { + pub id: Id, + pub name: String, +} + +impl Data { + // All of data's methods that aren't from traits. +} + +impl SomeTrait for Data { + // Implementations for traits in separate impl blocks. +} +``` + +
+- Inheritance comes with a number of downsides: + +- Heterogeneous by default: + + Class inheritance implicitly allows types of different classes to be used + interchangeably, without being able to specify a concrete type or if a type is + identical to another. + + For operations like equality, comparison this allows for comparison and + equality that throws and error or otherwise panics. + +- Multiple sources of truth for what makes up a data structure and how it + behaves: + + A type's fields are obscured by the inheritance hierarchy. + + A type's methods could be overriding a parent type or be overridden by a child + type, it's hard to tell what the behavior of a type is in complex codebases + maintained by multiple parties. + +- Dynamic dispatch as default adds overhead from vtable lookups: + + For dynamic dispatch to work, there needs to be somewhere to store information + on what methods to call and other pieces of runtime-known pieces of + information on the type. + + This store is the `vtable` for a value. Method calls will require more + dereferences than calling a method for a type that is known at compile time. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/03-switch-perspective.md b/src/idiomatic/polymorphism/from-oop-to-rust/03-switch-perspective.md new file mode 100644 index 000000000000..a78ec2eae4c9 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/03-switch-perspective.md @@ -0,0 +1,53 @@ +--- +minutes: 5 +--- + +# Inheritance from Rust's Perspective + +```rust +// Data +pub struct Data { + id: usize, + name: String, +} + +// Concrete behavior +impl Data { + fn new(id: usize, name: impl Into) -> Self { + self { id, name: name.into() } + } +} + +// Abstract behavior +trait Named { + fn name(&self) -> &str; +} + +// Instanced behavior +impl Named for Data { + fn name(&self) -> &str { + &self.name + } +} +``` + +- From Rust's perspective, one where Inheritance was never there, introducing + inheritance would look like muddying the water between types and traits. + +- A type is a concrete piece of data and its associated behavior. + + A trait is abstract behavior that must be implemented by a type. + + A class is a combination of data, behavior, and overrides to that behavior. + +- Coming from rust, an inheritable class looks like a type that is also a trait. + +- This is not an upside, as we can no longer reason about concrete types. + +- Without being able to separate the two, it becomes difficult to reason about + generic behavior vs concrete specifics, because in OOP these two concepts are + tied up in each other. + +- The convenience of flat field access and DRY in type definitions is not worth + the loss in specificity between writing code that delineates between behavior + and data. diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/04-supertraits.md b/src/idiomatic/polymorphism/from-oop-to-rust/04-supertraits.md new file mode 100644 index 000000000000..50bd99f30dee --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/04-supertraits.md @@ -0,0 +1,34 @@ +--- +minutes: 5 +--- + +# "Inheritance" in Rust: Supertraits + +```rust +pub trait SuperTrait {} + +pub trait Trait: SuperTrait {} +``` + +
+- In Rust, traits can depend on other traits. We're already familiar with Traits being able to have Supertraits. + +- This looks superficially similar to inheritance. + +- This is a mechanism like inheritance, but separates the data from the + behavior. + +- Keeps behavior in a state where it's easy to reason about. + +- Makes what we aim to achieve with "multiple inheritance" easier too: + + We only care about what behavior a type is capable of at the point where we + clarify we want that behavior (when bounding a generic by traits). + + By specifying multiple traits on a generic, we know that the type has the + methods of all those traits. + +- Does not involve inheritance of fields. A trait doesn't expose fields, only + methods and associated types / constants. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/05-composition.md b/src/idiomatic/polymorphism/from-oop-to-rust/05-composition.md new file mode 100644 index 000000000000..d86506840fd6 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/05-composition.md @@ -0,0 +1,33 @@ +--- +minutes: 5 +--- + +# Composition over Inheritance + +```rust +pub struct Uuid([u8; 16]); + +pub struct Address { + street: String, + city_or_province: String, + code: String, + country: String, +} + +pub struct User { + id: Uuid, + address: Address, +} +``` + +
+- Rather than mixins or inheritance, we compose types by creating fields of different types. + +This has downsides, largely in ergonomics of field access, but gives developers +a lot of control and clarity over what a type does and it has access to. + +- When deriving traits, make sure all the field types of a struct or variant + types of an enum implement that trait. Derive macros often assume all types + that compose a new type implement that trait already. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/01-dyn-trait.md b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/01-dyn-trait.md new file mode 100644 index 000000000000..0cffbb1de096 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/01-dyn-trait.md @@ -0,0 +1,36 @@ +--- +minutes: 5 +--- + +# `dyn Trait` for Dynamic Dispatch in Rust + +```rust +pub trait Trait {} + +impl Trait for i32 {} +impl Trait for String {} + +fn main() { + let int: &dyn Trait = &42i32; + let string = &dyn Trait = &("Hello dyn!".to_owned()); +} +``` + +
+- Dynamic Dispatch is a tool in Object Oriented Programming that is often used in places where one needs to care more about the behavior of a type than what the type is. + +In OOP languages, dynamic dispatch is often an _implicit_ process and not +something you can opt out of. + +In rust, we use `dyn Trait`: An opt-in form of dynamic dispatch. + +- For any trait that is _dyn compatible_ we can coerce a reference to a value of + that trait into a `dyn Trait` value. + +- We call these _trait objects_. Their type is not known at compile time, but + their behavior is: what is implemented by the trait itself. + +- When you _need_ OOP-style heterogeneous data structures, you can reach for + `Box`, but try to keep it homogeneous and generic-based first! + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/02-dyn-compatible.md b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/02-dyn-compatible.md new file mode 100644 index 000000000000..d420be6ac3c4 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/02-dyn-compatible.md @@ -0,0 +1,51 @@ +--- +minutes: 10 +--- + +# Dyn-compatible traits + +```rust +pub trait Trait { + + // dyn compatible + fn takes_self(&self); + + // dyn compatible, but you can't use this method when it's dyn + fn takes_self_and_param(&self, input: &T); + + // no longer dyn compatible + const ASSOC_CONST: i32; + + // no longer dyn compatible + fn clone(&self) -> Self +} +``` + +- Not all traits are able to be invoked as trait objects. A trait that can be + invoked is referred to as a _dyn compatible_ trait. + +- This was previously called _object safe traits_ or _object safety_. + +- Dynamic dispatch offloads a lot of compile-time type information into runtime + vtable information. + + If a concept is incompatible with what we can meaningfully store in a vtable, + either the trait stops being dyn compatible or those methods are excluded from + being able to be used in a dyn context. + +- A trait is dyn-compatible when all its supertraits are dyn-compatible and when + it has no associated constants/types, and no methods that depend on generics. + +- You'll most frequently run into dyn incompatible traits when they have + associated types/constants or return values of `Self` (i.e. the Clone trait is + not dyn compatible.) + + This is because the associated data would have to be stored in vtables, taking + up extra memory. + + For methods like `clone`, this disqualifies dyn compatibility because the + output type depends on the concrete type of `self`. + +ref: + +- https://doc.rust-lang.org/1.91.1/reference/items/traits.html#r-items.traits.dyn-compatible diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/03-dyn-vs-generics.md b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/03-dyn-vs-generics.md new file mode 100644 index 000000000000..74f108f9adcc --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/03-dyn-vs-generics.md @@ -0,0 +1,39 @@ +--- +minutes: 5 +--- + +# Generic Function Parameters vs dyn Trait + +We have two means of writing polymorphic functions, how do they compare? + +```rust +fn print_display(t: &T) { + println!("{}", t); +} + +fn print_display_dyn(t: &dyn std::fmt::Display) { + println!("{}", t); +} + +fn main() { + let int = 42i32; + // Monomorphized to a unique function for i32 inputs. + print_display(&int); + // One per + print_display_dyn(&int); +} +``` + +- We can write polymorphic functions over generics or over trait objects. + +- When writing functions with generic parameters, for each unique type that + substitutes a parameter a new version of that function is generated. + + We went over this in monomorphization: in exchange for binary size, we gain a + greater capacity for optimization. + +- When writing functions that take a trait object, only one version of that + function will exist in the final binary (not counting inlining.) + +- Generic parameters are zero-cost other than binary size. Types must be + homogenous (all instances of T can only be the same type). diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/04-limits.md b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/04-limits.md new file mode 100644 index 000000000000..f5387fb7c7f0 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/04-limits.md @@ -0,0 +1,40 @@ +--- +minutes: 5 +--- + +# Limits of Trait Objects + +```rust +use std::any::Any; + +pub trait Trait: Any {} + +impl Trait for i32 {} + +fn main() { + dbg!(size_of::()); // 4 bytes, owned value + dbg!(size_of::<&i32>()); // 8 bytes, reference + dbg!(size_of::<&dyn Trait>()); // 16 bytes, wide pointer +} +``` + +
+- Trait objects are a limited way of solving problems. + +- If you want to downcast to a concrete type from a trait object, you will need + to specify that the trait in question has Any as a supertrait or that the + trait object is over the main trait and `Any`. + + Even then, you will still need to cast a `dyn MyTrait` to `dyn Any` + +- Trait objects have overhead in memory, they are "wide pointers" that need to + hold not just the pointer to the data itself but another pointer for the + vtable. + +- Trait objects, being dynamically sized types, can only be used practically via + reference or pointer types. + + There is a baseline overhead of dereferencing the value and relevant trait + methods when using trait objects. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/05-heterogeneous.md b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/05-heterogeneous.md new file mode 100644 index 000000000000..dace789fe1cc --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/05-heterogeneous.md @@ -0,0 +1,41 @@ +--- +minutes: 2 +--- + +# Heterogeneous data with `dyn trait` + +```rust +pub struct Lambda; + +impl std::fmt::Display for Lambda { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "λ") + } +} + +pub struct Heterogeneous { + pub collection: Vec>, +} + +fn main() { + let heterogeneous = Heterogeneous { + collection: vec![ + Box::new(42u32), + Box::new("Woah".to_string()), + Box::new(Lambda), + ], + }; + for item in heterogeneous.collection { + // We know "item" implements Display, but we know nothing else! + println!("Display output: {}", item); + } +} +``` + +
+- `dyn Trait`, being a dynamic dispatch tool, lets us store heterogeneous data users can extend in collections. + +- In this example, we're storing types that all implement `std::fmt::Display` + and printing all items in that collection to screen. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/06-any-trait.md b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/06-any-trait.md new file mode 100644 index 000000000000..0d7a3ae63e30 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/06-any-trait.md @@ -0,0 +1,47 @@ +--- +minutes: 10 +--- + +# Any Trait and Downcasting + +```rust +use std::any::Any; + +#[derive(Debug)] +pub struct ThisImplementsAny; + +fn take_any(t: &T) {} + +fn main() { + let is_an_any = ThisImplementsAny; + take_any(&is_an_any); + + let dyn_any: &dyn Any = &is_an_any; + dbg!(dyn_any.type_id()); + dbg!(dyn_any.is::()); + let is_downcast: Option<&ThisImplementsAny> = dyn_any.downcast_ref(); + dbg!(is_downcast); +} +``` + +
+- The `Any` trait allows us to downcast values back from dyn values into concrete values. + +This is an auto trait: like Send/Sync/Sized, it is automatically implemented for +any type that meets specific criteria. + +The criteria for Any is that a type is `'static`. That is, the type does not +contain any non-`'static` lifetimes within it. + +- Any offers two related behaviors: downcasting, and runtime checking of types + being the same. + + In the example above, we see the ability to downcast from `Any` into + `ThisImplementsAny` automatically. + + We also see `Any::is` being used to check to see what type the value is. + +- `Any` does not implement reflection for a type, this is all you can do with + `Any`. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/07-pitfalls.md b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/07-pitfalls.md new file mode 100644 index 000000000000..8e85335c11c8 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/06-dynamic-dispatch/07-pitfalls.md @@ -0,0 +1,64 @@ +--- +minutes: 10 +--- + +# Pitfall: Reaching too quickly for `dyn Trait` + +```rust +use std::any::Any; + +pub trait AddDyn: Any { + fn add_dyn(&self, rhs: &dyn AddDyn) -> Box; +} + +impl AddDyn for i32 { + fn add_dyn(&self, rhs: &dyn AddDyn) -> Box { + if let Some(downcast) = (rhs as &dyn Any).downcast_ref::() { + Box::new(self + downcast) + } else { + Box::new(*self) + } + } +} + +fn main() { + let i: &dyn AddDyn = &42; + let j: &dyn AddDyn = &64; + let k: Box = i.add_dyn(j); + dbg!((k.as_ref() as &dyn Any).is::()); + dbg!((k.as_ref() as &dyn Any).downcast_ref::()); +} +``` + +- Coming from an OOP background, it's understandable to reach for this dynamic + dispatch tool as early as possible. + +- This is not the preferred way of doing things, trait objects put us in a + situation where we're exchanging knowledge of a type that both the developer + and compiler has for flexibility. + +- The above example takes things to the absurd: If adding numbers were tied up + in the dynamic dispatch process, it would be difficult to do anything at all. + + But dynamic dispatch is often hidden in a lot of programming languages: here's + it is more explicit. + + In the `i32` implementation of `AddDyn`, first we need to attempt to downcast + the `rhs` argument to the same type as `i32`, silently failing if this isn't + the case. + + Then we need to allocate the new value on the heap, because if we're keeping + this in the world of dynamic dispatch then we need to do this. + + Once we've added two values together, if we want to view them we must downcast + them again into a "real" type we can print out given the trait bounds tied up + in the operation so far. + +- Ask the class: Why can't we just add Display bounds in `main` to be able to + print things as-is? + + Answer: Because add_dyn returns only a `dyn AddDyn`, we lose information about + what the type implements between the argument type and return type. Even if + the inputs implement `Display`, the return type does not. + +- This leads to less performant code which is harder to understand diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/07-sealed-traits.md b/src/idiomatic/polymorphism/from-oop-to-rust/07-sealed-traits.md new file mode 100644 index 000000000000..e526d20f424e --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/07-sealed-traits.md @@ -0,0 +1,47 @@ +# Sealed traits for Polymorphism users cannot extend + +```rust +// crate can access the "sealed" module and its trait, but projects that +// depend on it cannot. +mod sealed { + pub trait Sealed {} + impl Sealed for String {} + impl Sealed for Vec {} + //... +} + +pub trait APITrait: sealed::Sealed { + /* methods */ +} +impl APITrait for String {} +impl APITrait for Vec {} +``` + +
+- Motivation: We want trait-driven code in a crate, but we don't want projects that depend on this crate to be able to implement a trait. + +Why? + +The trait could be considered unstable for downstream-implementations at this +point in time. + +Alternatively: Domain is high-risk for naive implementations of a trait (such as +cryptography). + +- The mechanism we use to do this is restricting access to a supertrait, + preventing downstream users from being able to implement that trait for their + types. + +- Why not just use enums? + + - Enums expose implementation details – "this works for these types". + + - Users need to use variant constructors of an enum to use the API. + + - Users can use the enum as a type in their own code, and when the enum + changes users need to update their code to match those changes. + + - Enums require branching on variants, whereas sealed traits lets the compile + specify monomophized functions for each type. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/08-sealing-with-enums.md b/src/idiomatic/polymorphism/from-oop-to-rust/08-sealing-with-enums.md new file mode 100644 index 000000000000..e02cd1f3a492 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/08-sealing-with-enums.md @@ -0,0 +1,45 @@ +--- +minutes: 5 +--- + +# Sealing with Enums + +```rust +pub enum GetSource { + WebUrl(String), + BytesMap(BTreeMap>), +} + +impl GetSource { + fn get(&self, url: &str) -> Option<&Vec> { + match self { + WebUrl(source) => unimplemented!(), + BytesMap(map) => map.get(url), + } + } +} +``` + +
+- Motivation: API is designed around a specific list of types that are valid for it, users of the API are not expected to extend it. + +- Enums in Rust are _algebraic data types_, we can define different structures + for each variant. + + For some domains, this might be enough polymorphism for the problem. + Experiment and see what works, what solutions seem to make more sense. + +- By having the user-facing part of the API refer to an enum, users know what + types are valid inputs and can construct those types using the available + methods to do so. + + - If the types that make up the enum have invariants that the API internally + upholds, and the only way users can construct those types is through + constructors that build and maintain those invariants, then you can be sure + that inputs to a generic method uphold their invariants. + + - If the types that make up the enum instead are types the user can freely + construct, then sanitisation and interpretation may need to be taken into + consideration. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/09-sticking-with-traits.md b/src/idiomatic/polymorphism/from-oop-to-rust/09-sticking-with-traits.md new file mode 100644 index 000000000000..8b2eb1a8e63f --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/09-sticking-with-traits.md @@ -0,0 +1,36 @@ +--- +minutes: 2 +--- + +# Traits for Polymorphism users can extend + +```rust +// Crate A + +pub trait Trait { + fn use_trait(&self) {} +} + +// Crate B, depends on A + +pub struct Data(u8); + +impl Trait for Data {} + +fn main() { + let data = Data(7u8); + data.use_trait(); +} +``` + +
+- We've already covered normal traits at length, but compared to enums and sealed traits they allow users to extend an API by implementing the behavior that API asks of them. + +This ability for users to extend is powerful for a number of domains, from +serialization to abstract representations of hardware and type safe linear +algebra. + +- If a trait is exposed publicly in a crate, a user depending on that crate can + implement that trait for types they define. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/10-problem-solving.md b/src/idiomatic/polymorphism/from-oop-to-rust/10-problem-solving.md new file mode 100644 index 000000000000..0226bb10beb2 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/10-problem-solving.md @@ -0,0 +1,76 @@ +--- +minutes: 15 +--- + +# Problem solving: Break Down the Problem + +```rust +// Problem: implementing a GUI API + +// Question: What's the minimum useful behavior for a drawing API? +pub trait DrawApi { + fn arc(&self, center: [f32; 2], radius: f32, start_angle: f32, end_angle: f32); + fn line(&self, start: [f32; 2], end: [f32; 2]); +} + +pub struct TextDraw; + +impl DrawApi for TextDraw { + fn arc(&self, center: [f32; 2], radius: f32, start_angle: f32, end_angle: f32) { + println!("arc of radius ") + } +} + +// Question: What's a good API for users? + +pub trait Draw { + pub fn draw(&self, surface: &mut T); +} + +pub struct Rect { + start: [f32; 2], + end: [f32; 2], +} + +impl Draw for Rect { + pub fn draw(&self, surface: &mut T) { + surface.line([self.start[0], self.start[1]], [self.end[0], self.start[1]]); + surface.line([self.end[0], self.start[1]], [self.end[0], self.end[1]]); + surface.line([self.end[0], self.end[1]], [self.start[0], self.end[1]]); + surface.line([self.start[0], self.end[1]], [self.start[0], self.start[1]]); + } +} +``` + +
+- You're already adept at breaking down problems, but you're likely used to reaching for OOP-style methods. + +This isn't a drastic change, it just requires re-ordering the way you approach +things. + +- Try to solve the problem with either Generics & Traits or Enums first. + + Does the problem require a specific set of types? An enum may be the cleanest + way of solving this problem. + + Does the problem really care about the specifics of the types involved, or can + behavior be focused on? + +- Organize your problem solving around finding a minimum viable amount of + knowledge to implement something. + + Does a trait already exist for this use case? If so, use it! + +- If you really do need heterogeneous collections, use them! They exist in rust + as a tool for a reason. + + Be aware of the XY problem: a problem may seem most easily addressable by one + solution, but it might not tackle the root cause and could lead to new + difficult problems popping up in the future. + + That is, be certain that dynamic dispatch with trait objects is what you need + before you commit to using them. + + Be certain that traits are what you need before you commit to using them. + +
diff --git a/src/idiomatic/polymorphism/refresher.md b/src/idiomatic/polymorphism/refresher.md new file mode 100644 index 000000000000..b8d10fd2b79c --- /dev/null +++ b/src/idiomatic/polymorphism/refresher.md @@ -0,0 +1,14 @@ +--- +minutes: 2 +--- + +# Refresher + +Basic features of Rust's generics and polymorphism. + +```rust +``` + +
+ +
diff --git a/src/idiomatic/polymorphism/refresher/01-traits.md b/src/idiomatic/polymorphism/refresher/01-traits.md new file mode 100644 index 000000000000..3254ec78b455 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/01-traits.md @@ -0,0 +1,62 @@ +--- +minutes: 10 +--- + +# Traits, Protocols, Interfaces + +```rust +trait Reciever { + fn send(&self, message: &str); +} + +struct Email { + email: String, +} + +impl Reciever for Email { + fn send(&self, message: &str) { + println!("Email to {}: {}", self.email, message); + } +} + +struct ChatId { + uuid: [u8; 16], +} + +impl Reciever for ChatId { + fn send(&self, message: &str) { + println!("Chat message sent to {}: {}", self.uuid, message); + } +} +``` + +
+- Rust's concept of polymorphism and generics is heavily built around traits. + +- Traits are requirements on a type in a generic context. + +- Requirements function much like a compile-time checked duck typing. + + Duck typing is a concept from the practice of dynamic, untyped languages like + Python, "if it walks like a duck and quacks like a duck, it's a duck." + + That is, types with the methods and fields expected by a function are all + valid inputs for that function. If a type implements methods, it is that type + in a duck-typing context. + + Traits behave like a static duck typing mechanism, in that we specify behavior + rather than type. But we get the compile-time checks on if that behavior does + really exist. + +- Alternatively: Traits are like collections of propositions, and implementing a + trait for a type is a proof that the type can be used wherever the trait is + asked for. + + Traits have required methods, implementing those methods is the proof that a + type has the required behavior. + +reference: + +- https://doc.rust-lang.org/reference/items/traits.html + +
diff --git a/src/idiomatic/polymorphism/refresher/02-trait-bounds.md b/src/idiomatic/polymorphism/refresher/02-trait-bounds.md new file mode 100644 index 000000000000..4b5b31481cec --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/02-trait-bounds.md @@ -0,0 +1,35 @@ +--- +minutes: 5 +--- + +# Trait Bounds on Generics + +```rust +use std::fmt::Display; + +fn print_with_length(item: T) { + println!("Item: {}", item); + println!("Length: {}", item.to_string().len()); +} + +fn main() { + let number = 42; + let text = "Hello, Rust!"; + + print_with_length(number); // Works with integers + print_with_length(text); // Works with strings +} +``` + +- Traits are most commonly used as bounds on generic type parameters for a + function or method. + + Without a trait bound on a generic type parameter, we don't have access to any + behavior to write functions and methods with. + + Trait bounds allow us to specify the minimum viable behavior of a type for it + to work in generic code. + +ref: + +- https://doc.rust-lang.org/reference/trait-bounds.html diff --git a/src/idiomatic/polymorphism/refresher/03-deriving-traits.md b/src/idiomatic/polymorphism/refresher/03-deriving-traits.md new file mode 100644 index 000000000000..8cba66e1b0a4 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/03-deriving-traits.md @@ -0,0 +1,47 @@ +--- +minutes: 10 +--- + +# Deriving Traits + +```rust +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +struct BufferId([u8; 16]); + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +struct DrawingBuffer { + target: [u8; 16], + commands: Vec, +} +``` + +
+- Many traits, protocols, interfaces, have trivial implementations that would be easy to mechanically write. + +- Definitions of types (their syntax trees) can be fed to procedural macros + (compiler plugins) to automatically generate implementations of traits. + + These macros have to be authored by someone, the compiler cannot figure out + everything by itself. + +- Many traits have a naive, obvious implementation. Mostly implementations that + depend on all fields or variants already implementing the trait. + + `PartialEq`/`Eq` can be derived on types whose fields / variants all implement + those traits fairly easily: line up the fields / variants, if any of them + don't match then the equality check returns false. + +- Derives let us avoid boilerplate mechanically and predictably, the authors of + a derive implementation likely authored the trait the derive was implemented + with the proper semantics of a trait in mind. + +- Ask the class: Have the students had to deal with a codebase where most of the + code was trivial boilerplate? + +- This is similar to Haskell's `deriving` system. + +references: + +- https://doc.rust-lang.org/reference/attributes/derive.html#r-attributes.derive + +
diff --git a/src/idiomatic/polymorphism/refresher/04-default-impls.md b/src/idiomatic/polymorphism/refresher/04-default-impls.md new file mode 100644 index 000000000000..5b72f75a053a --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/04-default-impls.md @@ -0,0 +1,41 @@ +--- +minutes: 5 +--- + +# Default method implementations + +```rust +pub trait CollectLeaves { + type Leaf; + + // Required Method + fn collect_leaves_buffered(&self, buf: &mut Vec); + + // Default implementation + fn collect_leaves(&self) -> Vec { + let mut buf = vec![]; + self.collect_leaves_buffered(&mut buf); + buf + } +} +``` + +
+- Traits often have methods that are implemented for you already, once you implement the required methods. + +- A trait method has a default implementation if the function body is present. + This implementation can be written + +- Often you'll see methods that provide the broad functionality that is + necessary to implement (like `Ord`'s `compare`) with default implementations + for functions that can be implemented in terms of those methods (like `Ord`'s + `max`/`min`/`clamp`). + +- Default methods can be overridden by derive macros, as derive macros produce + arbitrary ASTs in the implementation. + +ref: + +- https://doc.rust-lang.org/reference/items/traits.html#r-items.traits.associated-item-decls + +
diff --git a/src/idiomatic/polymorphism/refresher/05-supertraits.md b/src/idiomatic/polymorphism/refresher/05-supertraits.md new file mode 100644 index 000000000000..a8e8e790fcf0 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/05-supertraits.md @@ -0,0 +1,43 @@ +# Supertraits / Trait Dependencies + +Traits can be extended by new traits. + +```rust,compile_fail +pub trait DeviceId { + /* trait for device ID types */ +} + +pub trait GraphicsDevice: DeviceId { + /* Graphics device specifics */ +} + +// From stdlib + +pub trait Ord: PartialOrd { + /* methods for Ord */ +} +``` + +
+- When authoring a trait, you can specify traits that a type must also. These are called _Supertraits_. + +For the example above, any type that implements `GraphicsDevice` must also +implement `DeviceId`. + +- These hierarchies of traits let us design systems around the behavior of + complex real-world taxonomies (like machine hardware, operating system + specifics). + +- This is distinct from object inheritance! But it looks similar. + + - Object inheritance allows for overrides and brings in the behavior of the + inherited types by default. + + - A trait having a supertrait doesn't mean that trait can override method + implementations as default implementations. + +ref: + +- https://doc.rust-lang.org/reference/items/traits.html?highlight=supertrait#r-items.traits.supertraits + +
diff --git a/src/idiomatic/polymorphism/refresher/06-blanket-impls.md b/src/idiomatic/polymorphism/refresher/06-blanket-impls.md new file mode 100644 index 000000000000..e2394a035d48 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/06-blanket-impls.md @@ -0,0 +1,57 @@ +--- +minutes: 10 +--- + +# Blanket trait implementations + +When a trait is local, we can implement it for as many types as we like. How far +can we take this? + +```rust +pub trait PrettyPrint { + fn pretty_print(&self); +} + +// A blanket implementation! If something implements Display, it implements +// PrettyPrint. +impl PrettyPrint for T +where + T: std::fmt::Display, +{ + fn pretty_print(&self) { + println!("{self}") + } +} +``` + +
+- The subject of a trait implementation at the definition site of a trait can be anything, including `T` with no bounds. + +We can't do anything with a `T` we don't know nothing about, so this is +uncommon. + +- Conditional blanket implementations are much more useful and you are more + likely to see and author them. + + These implementations will have a bound on the trait, like + `impl ToString for T {...}` + + In the example above we have a blanket implementation for all types that + implement Display, the implementation has one piece of information available + to it from the trait bounds: it implements `Display::fmt`. + + This is enough to write an implementation for pretty printing to console. + +- Do be careful with these kinds of implementations, as it may end up preventing + users downstream from implementing a more meaningful. + + The above isn't written for `Debug` as that would mean almost all types end up + implementing `PrettyPrint`, and `Debug` is not semantically similar to + `Display`: It's meant for debug output instead of something more + human-readable. + +ref: + +- https://doc.rust-lang.org/reference/glossary.html#blanket-implementation + +
diff --git a/src/idiomatic/polymorphism/refresher/07-conditional-methods.md b/src/idiomatic/polymorphism/refresher/07-conditional-methods.md new file mode 100644 index 000000000000..649bb973c375 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/07-conditional-methods.md @@ -0,0 +1,44 @@ +--- +minutes: 5 +--- + +# Conditional Method Implementations + +```rust +// No trait bounds on the type definition. +pub struct Value(T); + +// Instead bounds are put on the implementations for the type. +impl Value { + fn log(&self) { + println!("{}", self.0); + } +} + +// alternatively +impl Value { + // Specifies the trait bound in a where expression + fn log_error(&self) + where + T: std::error::Error, + { + eprintln!("{}", self.0); + } +} +``` + +
+- When authoring a type with generic parameters, we can write implementations for that type that depend on what the parameters are or what traits they implement. + +- These methods are only available when the type meets those conditions. + +- For things like ordered sets, where you'd want the inner type to always be + `Ord`, this is the preferred way of putting a trait bound on a parameter of a + type. + + We don't put the definition on the type itself as this would cause downstream + issues for everywhere the type is mentioned with a generic parameter. + + We can maintain invariants just fine with conditional method implementations. + +
diff --git a/src/idiomatic/polymorphism/refresher/08-orphan-rule.md b/src/idiomatic/polymorphism/refresher/08-orphan-rule.md new file mode 100644 index 000000000000..00a983c749d3 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/08-orphan-rule.md @@ -0,0 +1,66 @@ +--- +minutes: 10 +--- + +# Orphan Rule + +What prevents users from writing arbitrary trait implementations for any type? + +```rust,compile_fail +// Crate `postgresql-bindings` + +pub struct PostgresqlConn(/* details */); + +// Crate `database-traits`, depends on `postgresql-bindings` + +pub trait DbConnection { + /* methods */ +} + +impl DbConnection for PostgresqlConn {} // ✅, `DbConnection` is local. + +// Crate `mycoolnewdb` depends on `database-traits` + +pub struct MyCoolNewDbConn(/* details */); + +impl DbConnection for MyCoolNewDbConn {} // ✅, `MyCoolNewDbConn` is local. + +// Neither `PostgresqlConn` or `DbConnection` are local to `mycoolnewdb`. +// This would lead to two implementations of `DbConnection` for PostgresqlConn! +impl DbConnection for PostgresqlConn {} // ❌🔨 +``` + +
+- Rust traits should never be able to be implemented twice in its ecosystem. Two implementations of the same trait for the same type is a conflict with no solution. + +We can prevent this within a crate by detecting if there are multiple +definitions and disallowing it, but what about between crates in the entire rust +ecosystem? + +- Types are either _local_ to a crate, they are defined there, or they're not. + + In the example's "crates", `PostgresqlConn` is local to `postgresql-bindings`, + `MyCoolNewDbConn` is local to `mycoolnewdb`. + +- Traits are also either _local_ to a crate, they are defined there, or they're + not. + + Again in the example, the `DbConnection` trait is local to `database-traits`. + +- If something is local, you can write trait implementations for it. + + If the trait is local, you can write implementations of that trait for any + type. + + If the type is local, you can write any trait implementations for that type. + +- Outside of these boundaries, trait implementations cannot be written. + + This keeps implementations "coherent": Only one implementation of a trait for + a type can exist across crates. + +ref: + +- https://doc.rust-lang.org/stable/reference/items/implementations.html#r-items.impl.trait.orphan-rule + +
diff --git a/src/idiomatic/polymorphism/refresher/09-sized.md b/src/idiomatic/polymorphism/refresher/09-sized.md new file mode 100644 index 000000000000..0daf0aeb2450 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/09-sized.md @@ -0,0 +1,33 @@ +--- +minutes: 2 +--- + +# Statically Sized and Dynamically Sized Types + +```rust +pub struct AlwaysSized(T); + +pub struct OptionallySized(T); + +type Dyn1 = OptionallySized; +``` + +Motivation: Being able to specify between types whose size are known and compile +time and types whose size are known at runtime is useful for + +- The Sized trait is automatically implemented by types with a known size at + compile-time. + + This trait is also automatically added to any type parameter that doesn't + opt-out of being sized. + +- Most types implement `Sized`: they have a compile-time known size. + + Types like `[T]`, `str` and `dyn Trait` are all dynamically sized types. Their + size is stored as part of the reference to the value of that type. + +- Type parameters automatically implement `Sized` unless specified. + +ref: + +- https://doc.rust-lang.org/stable/reference/dynamically-sized-types.html#r-dynamic-sized diff --git a/src/idiomatic/polymorphism/refresher/10-monomorphization.md b/src/idiomatic/polymorphism/refresher/10-monomorphization.md new file mode 100644 index 000000000000..675f2bec3836 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/10-monomorphization.md @@ -0,0 +1,42 @@ +--- +minutes: 10 +--- + +# Monomorphization and Binary Size + +```rust +fn print_vec(debug_vec: &Vec) { + for item in debug_vec { + println!("{:?}", item); + } +} + +fn main() { + let ints = vec![1u32, 2, 3]; + let floats = vec![1.1f32, 2.2, 3.3]; + + // instance one, &Vec -> () + print_vec(&ints); + // instance two, &Vec -> () + print_vec(&floats); +} +``` + +
+- Each instance of a function or type with generics gets transformed into a unique, concrete version of that function at compile time. Generics do not exist at runtime, only specific types. + +- This comes with a strong baseline performance and capacity for optimization, + but at a cost of binary size and compile time. + +- There are plenty of ways to trim binary size and compilation times[, but we're + not covering them here?] + +- Pay for what you use: Binary size increase of monomorphization is only + incurred for instances of a type or functions on a type used in the final + program or dynamic library. + +- When to care: Monomorphization impacts compile times and binary size. In + circumstances like WebAssembly in-browser or embedded systems development, you + may want to be mindful about designing with generics in mind. + +
diff --git a/src/idiomatic/polymorphism/working-with-generics.md b/src/idiomatic/polymorphism/working-with-generics.md new file mode 100644 index 000000000000..e69de29bb2d1