-
Notifications
You must be signed in to change notification settings - Fork 2.1k
First pass at polymorphism improvements #3264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
|
||
| <details> | ||
|
|
||
| - 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? | ||
|
|
||
| </details> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String, JsonValue>), | ||
| Array(Vec<JsonValue>), | ||
| String(String), | ||
| Number(f64), | ||
| Bool(bool), | ||
| Null, | ||
| } | ||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| - 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. | ||
|
|
||
| </details> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }), | ||
| ]; | ||
| } | ||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| - 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. | ||
|
|
||
| </details> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"), | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| - 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. | ||
|
|
||
| </details> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| # 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 | ||
| 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); | ||
| } | ||
| } | ||
|
|
||
| impl Pet for Cat { | ||
| fn talk(&self) { | ||
| println!("Meow! I am {} years old", self.age); | ||
| } | ||
| } | ||
|
|
||
| enum AnyPet { | ||
| Dog(Dog), | ||
| Cat(Cat), | ||
| } | ||
|
|
||
| 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)); | ||
| } | ||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| - 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`. | ||
|
|
||
| </details> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,14 +26,8 @@ fn main() { | |
|
|
||
| <details> | ||
|
|
||
| - 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. | ||
|
Comment on lines
-29
to
-34
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason for dropping this prose? In my mind this explanation is useful we assume if our audience is coming from a particularly OO-heavy background, e.g. C++ or Java, where subclassing is pervasive. But on the other hand, that difference is also covered in Fundamentals when we talk about dynamic dispatch and memory layouts of trait objects. |
||
|
|
||
| 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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<T> { ... } | ||
| ``` | ||
|
|
||
| ```rust,editable | ||
| let ints: Vec<i32> = Vec::new(); | ||
| vec.push(123); | ||
| vec.push(456); | ||
|
|
||
| let strings: Vec<&str> = Vec::new(); | ||
| vec.push("hello"); | ||
| vec.push("goodbye"); | ||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| - 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`. | ||
|
|
||
| </details> |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,31 @@ | ||||||
| # Kinds of Polymorphism | ||||||
|
|
||||||
| In Rust we have 3 different mechanism for doing polymorphism: | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| - **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. | ||||||
|
|
||||||
| <details> | ||||||
|
|
||||||
| - 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. | ||||||
|
|
||||||
| </details> | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the perspective of "idiomatic Rust", maybe we should mention crates used to automate this pattern, e.g.
enum_dispatch?