Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

---
Expand Down
22 changes: 22 additions & 0 deletions src/idiomatic/polymorphism/enum-vs-dyn.md
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>
44 changes: 44 additions & 0 deletions src/idiomatic/polymorphism/enums.md
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>
50 changes: 50 additions & 0 deletions src/idiomatic/polymorphism/enums/heterogeneous.md
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>
32 changes: 32 additions & 0 deletions src/idiomatic/polymorphism/enums/inspecting.md
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>
90 changes: 90 additions & 0 deletions src/idiomatic/polymorphism/enums/traits.md
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
Expand Up @@ -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.

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.
Expand Down
41 changes: 41 additions & 0 deletions src/idiomatic/polymorphism/generics.md
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>
31 changes: 31 additions & 0 deletions src/idiomatic/polymorphism/polymorphism-kinds.md
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:

- **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>
Loading
Loading