From 7f4f37f02904d53d7563541c1059a775a51cbc43 Mon Sep 17 00:00:00 2001 From: Nicole LeGare Date: Thu, 13 Aug 2026 17:50:43 -0700 Subject: [PATCH 1/2] First pass at polymorphism improvements - Break things up into sections on generics, enums, and dyn. - Various non-OOP-specific slides moved into the new sections. - Add new intro slides contextualizing polymorphism and how it comes up in a static language like Rust, and introducing our 3 flavors of polymorphism. - Add slide directly comparing enums and dyn. - Add slides on enums and how they're used for polymorphism. --- src/SUMMARY.md | 29 ++++--- src/idiomatic/polymorphism/enum-vs-dyn.md | 22 ++++++ src/idiomatic/polymorphism/enums.md | 44 +++++++++++ .../polymorphism/enums/heterogeneous.md | 50 ++++++++++++ .../polymorphism/enums/inspecting.md | 32 ++++++++ src/idiomatic/polymorphism/enums/traits.md | 77 +++++++++++++++++++ .../dynamic-dispatch/dyn-trait.md | 10 +-- src/idiomatic/polymorphism/generics.md | 41 ++++++++++ .../polymorphism/polymorphism-kinds.md | 31 ++++++++ src/idiomatic/polymorphism/python.md | 55 +++++++++++++ .../polymorphism/refresher/supertraits.md | 4 +- 11 files changed, 374 insertions(+), 21 deletions(-) create mode 100644 src/idiomatic/polymorphism/enum-vs-dyn.md create mode 100644 src/idiomatic/polymorphism/enums.md create mode 100644 src/idiomatic/polymorphism/enums/heterogeneous.md create mode 100644 src/idiomatic/polymorphism/enums/inspecting.md create mode 100644 src/idiomatic/polymorphism/enums/traits.md create mode 100644 src/idiomatic/polymorphism/generics.md create mode 100644 src/idiomatic/polymorphism/polymorphism-kinds.md create mode 100644 src/idiomatic/polymorphism/python.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 34f9edc9549d..40b5a50ef8ee 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -522,7 +522,9 @@ SPDX-License-Identifier: CC-BY-4.0 - [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) + - [What is Polymorphism?](idiomatic/polymorphism/python.md) + - [Kinds of Polymorphism](idiomatic/polymorphism/polymorphism-kinds.md) + - [Generics](idiomatic/polymorphism/generics.md) - [Traits](idiomatic/polymorphism/refresher/traits.md) - [Trait Bounds](idiomatic/polymorphism/refresher/trait-bounds.md) - [Deriving Traits](idiomatic/polymorphism/refresher/deriving-traits.md) @@ -533,22 +535,27 @@ SPDX-License-Identifier: CC-BY-4.0 - [Orphan Rule](idiomatic/polymorphism/refresher/orphan-rule.md) - [Statically Sized and Dynamically Sized types](idiomatic/polymorphism/refresher/sized.md) - [Monomorphization and Binary Size](idiomatic/polymorphism/refresher/monomorphization.md) - - [From OOP to Rust](idiomatic/polymorphism/from-oop-to-rust.md) - - [Inheritance](idiomatic/polymorphism/from-oop-to-rust/inheritance.md) - - [Why no Inheritance in Rust?](idiomatic/polymorphism/from-oop-to-rust/why-no-inheritance.md) - - [Inheritance from Rust's Perspective](idiomatic/polymorphism/from-oop-to-rust/switch-perspective.md) - - ["Inheritance" in Rust and Supertraits](idiomatic/polymorphism/from-oop-to-rust/supertraits.md) - - [Composition over Inheritance](idiomatic/polymorphism/from-oop-to-rust/composition.md) - - [Trait Objects and Dynamic Dispatch](idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-trait.md) + - [Sealed Traits](idiomatic/polymorphism/from-oop-to-rust/sealed-traits.md) + - [Traits for Polymorphism users can extend](idiomatic/polymorphism/from-oop-to-rust/sticking-with-traits.md) + - [Enums](idiomatic/polymorphism/enums.md) + - [Inspecting Enums](idiomatic/polymorphism/enums/inspecting.md) + - [Heterogeneous Collections](idiomatic/polymorphism/enums/heterogeneous.md) + - [Re-exposing Traits](idiomatic/polymorphism/enums/traits.md) + - [Sealing with Enums](idiomatic/polymorphism/from-oop-to-rust/sealing-with-enums.md) + - [`dyn`](idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-trait.md) - [Dyn Compatibility](idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-compatible.md) - [Generics vs Trait Objects](idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-vs-generics.md) - [Limits of Trait Objects](idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/limits.md) - [Heterogeneous Collections](idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/heterogeneous.md) - [The `Any` Trait](idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/any-trait.md) - [Pitfall: Reaching too quickly for `dyn Trait`](idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/pitfalls.md) - - [Sealed Traits](idiomatic/polymorphism/from-oop-to-rust/sealed-traits.md) - - [Sealing with Enums](idiomatic/polymorphism/from-oop-to-rust/sealing-with-enums.md) - - [Traits for Polymorphism users can extend](idiomatic/polymorphism/from-oop-to-rust/sticking-with-traits.md) + - [Enums vs `dyn`](idiomatic/polymorphism/enum-vs-dyn.md) + - [From OOP to Rust](idiomatic/polymorphism/from-oop-to-rust.md) + - [Inheritance](idiomatic/polymorphism/from-oop-to-rust/inheritance.md) + - [Why no Inheritance in Rust?](idiomatic/polymorphism/from-oop-to-rust/why-no-inheritance.md) + - [Inheritance from Rust's Perspective](idiomatic/polymorphism/from-oop-to-rust/switch-perspective.md) + - ["Inheritance" in Rust and Supertraits](idiomatic/polymorphism/from-oop-to-rust/supertraits.md) + - [Composition over Inheritance](idiomatic/polymorphism/from-oop-to-rust/composition.md) - [Problem solving: Break Down the Problem](idiomatic/polymorphism/from-oop-to-rust/problem-solving.md) --- diff --git a/src/idiomatic/polymorphism/enum-vs-dyn.md b/src/idiomatic/polymorphism/enum-vs-dyn.md new file mode 100644 index 000000000000..0756a20d14f8 --- /dev/null +++ b/src/idiomatic/polymorphism/enum-vs-dyn.md @@ -0,0 +1,22 @@ +# Enums vs `dyn` + +Enums and `dyn` provide two different approaches to dynamic polymorphism, and in +many cases both approaches can be used to solve the same problem. The main +trade-offs between the two are: + +- Enums make "downcasting" to a subtype easy via pattern matching. Downcasting + is possible with `dyn` but is more cumbersome. +- `dyn` allows for downstream code to introduce new types, whereas enums do not. + +
+ +- When deciding whether to use an enum or `dyn`, there are two questions to ask: + + - Do I need to be able to downcast to the concrete subtype? Or do I primarily + expect to go through a trait interface without needing to know the concrete + type? + + - Do I know the full set of types up front, or do I need to allow downstream + code to extend the set of types I will be handling? + +
diff --git a/src/idiomatic/polymorphism/enums.md b/src/idiomatic/polymorphism/enums.md new file mode 100644 index 000000000000..ec99be1f937e --- /dev/null +++ b/src/idiomatic/polymorphism/enums.md @@ -0,0 +1,44 @@ +# Enums + +Sometimes we need to handle multiple different types at runtime. Enums are a +powerful tool that allows us to safely and robustly describe situations like +this. + +```rust,editable +use std::collections::HashMap; + +fn main() { + let number = parse_json("123"); + let array = parse_json("[456, true, false]"); + let object = parse_json(r#"{ "key": "value" }"#); +} + +fn parse_json(doc: &str) -> JsonValue { + todo!("Parse the JSON string...") +} + +enum JsonValue { + Object(HashMap), + Array(Vec), + String(String), + Number(f64), + Bool(bool), + Null, +} +``` + +
+ +- The other category of polymorphism is **dynamic polymorphism**, where we can + have different types of value at runtime, and we can't know statically which + type we'll have at any given time. + +- As an example, consider parsing a JSON string. There are several different + types of JSON value, and which one we return depends on the contents of the + input string. + +- Our `parse_json` function has to return a single, concrete type, and that type + needs to describe all of the possible types a JSON value can be. Enums are a + natural way of describing this kind of situation in Rust. + +
diff --git a/src/idiomatic/polymorphism/enums/heterogeneous.md b/src/idiomatic/polymorphism/enums/heterogeneous.md new file mode 100644 index 000000000000..1236acc40427 --- /dev/null +++ b/src/idiomatic/polymorphism/enums/heterogeneous.md @@ -0,0 +1,50 @@ +# Heterogeneous Collections with Enums + +Enums give us a way to create collections that can store different types of +element at runtime: + +```rust,editable +struct Dog { + name: String, +} + +struct Cat { + age: u8, +} + +enum AnyPet { + Dog(Dog), + Cat(Cat), +} + +fn main() { + let pets = vec![ + AnyPet::Dog(Dog { name: "Fido".into() }), + AnyPet::Cat(Cat { age: 19 }), + ]; +} +``` + +
+ +- A common situation where we might need dynamic polymorphism is when we want to + store different types of value in the same collection. + +- In the above example, we want to store both `Cat`s and `Dog`s in the same + list. `Vec` doesn't support this directly: All elements of the `Vec` must be + the same type. + +- Wrapping our two different pet types into a single `AnyPet` enum gives us a + unified type representation that can be stored in a `Vec`, while allowing + individual elements of the `Vec` to be different types. + +- This requires that we know all possible pet types up front, as we need to + explicitly list them as different variants of the `AnyPet` enum. This works + well for libraries or applications that define the full set of possible types, + but does not allow downstream users to extend our list of pet types. + +- Later we will see that we can do the same thing with `dyn`, which allows + downstream extension at the cost of being harder to downcast and requiring + dynamic dispatch. + +
diff --git a/src/idiomatic/polymorphism/enums/inspecting.md b/src/idiomatic/polymorphism/enums/inspecting.md new file mode 100644 index 000000000000..5562d816cf39 --- /dev/null +++ b/src/idiomatic/polymorphism/enums/inspecting.md @@ -0,0 +1,32 @@ +# Inspecting Enums + +We can easily inspect the contents of an enum using **pattern matching**: + +```rust,editable,compile_fail +fn do_json_stuff(json: &str) { + match parse_json(json) { + JsonValue::Object(obj) => println!("We got an object: {obj:?}"), + JsonValue::Array(array) => println!("We got an array: {array:?}"), + JsonValue::String(string) => println!("We got a string: {string:?}"), + JsonValue::Number(num) => println!("We got a number: {num}"), + JsonValue::Bool(b) => println!("We got a bool: {b}"), + JsonValue::Null => println!("We got a null"), + } +} +``` + +
+ +- Continuing with our JSON parsing example, we can easily determine which kind + of value we got by pattern matching on the resulting enum. + +- This makes enums a good fit for scenarios where we want to handle different + types at runtime, but want to retain type information and the ability to + directly inspect the concrete value. + +- This is a big advantage enums have over `dyn`: With `dyn` we can't easily + downcast to the specific concrete type, and are generally restricted to going + through the trait interface. Later we'll see that we can support downcasting + with `dyn`, but doing so requires extra setup that isn't necessary with enums. + +
diff --git a/src/idiomatic/polymorphism/enums/traits.md b/src/idiomatic/polymorphism/enums/traits.md new file mode 100644 index 000000000000..b3801542c739 --- /dev/null +++ b/src/idiomatic/polymorphism/enums/traits.md @@ -0,0 +1,77 @@ +# Re-exposing Traits + +Sometimes we use an enum to abstract over multiple types that implement the same +trait, and want the enum to also re-expose the trait's interface. + +```rust,editable,compile_fail +trait Pet { + fn talk(&self); +} + +impl Pet for Dog { + fn talk(&self) { + println!("Woof! My name is {}~!", self.name); + } +} + +impl Pet for Cat { + fn talk(&self) { + println!("Meow! I am {} years old", self.age); + } +} + +impl Pet for AnyPet { + fn talk(&self) { + match self { + Self::Dog(dog) => dog.talk(), + Self::Cat(cat) => cat.talk(), + } + } +} + +fn do_pet_stuff(pet: &impl Pet) { + pet.talk(); +} + +fn main() { + let cat = Cat { + age: 19, + }; + + let dog = Dog { + name: "Fido".into(), + }; + + do_pet_stuff(&cat); + do_pet_stuff(&dog); + do_pet_stuff(&AnyPet::Dog(dog)); + do_pet_stuff(&AnyPet::Cat(cat)); +} +``` + +
+ +- One drawback of using an enum for dynamic polymorphism is that if our + underlying types (`Cat` and `Dog` in this case) implement a trait (`Pet`), our + wrapper enum doesn't automatically expose that same trait interface. + +- We can generally implement the trait for the wrapper enum by matching on the + enum and dispatching to the corresponding trait method on the underlying + types. + +- This is dynamic dispatch, but using the enum's discriminant instead of a + vtable in order to lookup the correct function to call. + +- This is an ergonomic drawback of an enum vs `dyn`: The implementation of `Pet` + for `AnyPet` is pure boilerplate that we need to repeat each time we have a + situation like this, whereas `dyn` gives us this behavior purely from the + `Pet` impls on `Cat` and `Dog`. + +- The advantage of this approach is that we retain the useful properties of an + enum (e.g. the ability to pattern match on it) while also exposing a way to do + dynamic dispatch through the trait's interface. + +- This also enables us to use `AnyPet` with generic functions like + `do_pet_stuff`, which we can also do with `dyn`. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-trait.md b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-trait.md index 9467d88de6eb..a9a4344bddc3 100644 --- a/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-trait.md +++ b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-trait.md @@ -26,14 +26,8 @@ fn main() {
-- 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. +- Our other main mechanism of doing dynamic polymorphism is `dyn`, which gives + us dynamic dispatch through a trait interface. - For any trait that is _dyn compatible_ we can coerce a reference to a value of that trait into a `dyn Trait` value. diff --git a/src/idiomatic/polymorphism/generics.md b/src/idiomatic/polymorphism/generics.md new file mode 100644 index 000000000000..e284d37504c2 --- /dev/null +++ b/src/idiomatic/polymorphism/generics.md @@ -0,0 +1,41 @@ +# Generics + +Generics are used when we want to abstract over types, but we expect the users +of our code to know the concrete types. + +```rust +pub struct Vec { ... } +``` + +```rust,editable +let ints: Vec = Vec::new(); +vec.push(123); +vec.push(456); + +let strings: Vec<&str> = Vec::new(); +vec.push("hello"); +vec.push("goodbye"); +``` + +
+ +- Generics are our mechanism for **static polymorphism**, which is polymorphism + where the types are fully known at compile time. + +- One example of this is `Vec`, which is generic over the type of element it + stores. `Vec` itself is polymorphic: It's written in such a way that it + doesn't know what type of element will be stored in it. But in order to use a + `Vec`, you must specify a concrete type to use for the element. + +- Generics are a mechanism for **code reuse**: You have some common logic that + is fundamentally the same regardless of what specific type it handles, and + generics give you a way to abstract over those different types **without + duplicating logic**. + +- Note that there's no dynamism: The types must be fully known at compile time, + there's no way to select a type for `Vec`'s element at runtime. This means + that generics are not an option when we need **runtime polymorphism**. Later + we will look at two mechanisms for doing dynamic polymorphism: Enums and + `dyn`. + +
diff --git a/src/idiomatic/polymorphism/polymorphism-kinds.md b/src/idiomatic/polymorphism/polymorphism-kinds.md new file mode 100644 index 000000000000..cdd53f7aac29 --- /dev/null +++ b/src/idiomatic/polymorphism/polymorphism-kinds.md @@ -0,0 +1,31 @@ +# Kinds of Polymorphism + +In Rust we have 3 different mechanism for doing polymorphism: + +- **Generics** - Static polymorphism where code abstracts over types but the + types are fully known at compile time. +- **Enums** - Dynamic polymorphism where a fixed set of known types are selected + between at runtime. +- **`dyn`** - Dynamic polymorphism where any type meeting a particular trait + interface can be used. + +
+ +- When discussing polymorphism in Rust, it's helpful to differentiate between + **static** polymorphism and **dynamic** polymorphism. + + - **static polymorphism** is when we abstract over types, but the type + information is fully known by the compiler. This allows us to reuse code in + different type contexts without needing to add any runtime overhead, and we + have access to the full set of features that traits expose. This is + accomplished with **generics** in Rust. + + - **dynamic polymorphism** is when we need to select between different types + at runtime, and we don't know at compile time which specific type will be + used. When we have a fixed set of known types to choose from, we can use + **enums** to track at runtime which one we have. When we don't know ahead of + time which types may be used, e.g. if downstream users may introduce new + types that we don't know about, then we use **`dyn`** to allow + extensibility. + +
diff --git a/src/idiomatic/polymorphism/python.md b/src/idiomatic/polymorphism/python.md new file mode 100644 index 000000000000..6fda526d1704 --- /dev/null +++ b/src/idiomatic/polymorphism/python.md @@ -0,0 +1,55 @@ +# What is Polymorphism? + +In a dynamic language like Python, we don't have to do anything special to allow +a function to accept arguments of different types: + +```python,editable +def print_value(val): + print(val) + +print_value(123) +print_value("hello") +print_value({}) +``` + +But Rust's type system is extremely static, meaning that by default a function's +arguments are limited to exactly the type declared in the function signature: + +```rust,editable +fn print_value(val: i32) { + println!("{}", val); +} + +fn main() { + print_value(123); + // print_value("hello"); // 🛠️❌ Mismatched types! +} +``` + +
+ +- If you are coming from a dynamic language like Python, the concept of + polymorphism may be new to you because in dynamic languages everything is + inherently polymorphic. But Rust is a very statically-typed language, meaning + the compiler heavily restricts what types can be used where. + +- Static typing is a powerful tool that allows the compiler to enforce correct + usage of our APIs: If your function needs to be given an `i32` in order to + function correctly, the compiler won't allow a user of that function to pass + in a string. + +- But static typing is restrictive in cases where we want to write code that is + flexible in its handling of types. In the example on this slide, we might want + our `print_value` function to be able to print both numbers and strings, but + as written it can only accept `i32` values. + +- Edit `print_value` to make it generic using `Display` to print different kinds + of values: + + ```rust + fn print_value(val: impl std::fmt::Display) { + println!("{}", val); + } + ``` + +
diff --git a/src/idiomatic/polymorphism/refresher/supertraits.md b/src/idiomatic/polymorphism/refresher/supertraits.md index 50e27a1291db..aadd5d3dbb84 100644 --- a/src/idiomatic/polymorphism/refresher/supertraits.md +++ b/src/idiomatic/polymorphism/refresher/supertraits.md @@ -32,8 +32,8 @@ pub trait Ord: Eq + PartialOrd {
-- When authoring a trait, you can specify traits that a type must also. These - are called _supertraits_. +- When authoring a trait, you can specify traits that a type must also + implement. These are called _supertraits_. For the example above, any type that implements `Mammal` must also implement `Animal`. From 4d735f1af2e4ff299f09c6b11a6acb792d03139c Mon Sep 17 00:00:00 2001 From: Nicole LeGare Date: Fri, 14 Aug 2026 12:50:57 -0700 Subject: [PATCH 2/2] Don't hide the implementation details in enum traits slide --- src/idiomatic/polymorphism/enums/traits.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/idiomatic/polymorphism/enums/traits.md b/src/idiomatic/polymorphism/enums/traits.md index b3801542c739..620059422517 100644 --- a/src/idiomatic/polymorphism/enums/traits.md +++ b/src/idiomatic/polymorphism/enums/traits.md @@ -3,11 +3,19 @@ Sometimes we use an enum to abstract over multiple types that implement the same trait, and want the enum to also re-expose the trait's interface. -```rust,editable,compile_fail +```rust,editable trait Pet { fn talk(&self); } +struct Dog { + name: String, +} + +struct Cat { + age: u8, +} + impl Pet for Dog { fn talk(&self) { println!("Woof! My name is {}~!", self.name); @@ -20,6 +28,11 @@ impl Pet for Cat { } } +enum AnyPet { + Dog(Dog), + Cat(Cat), +} + impl Pet for AnyPet { fn talk(&self) { match self {