Skip to content
Open
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: 29 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down
30 changes: 30 additions & 0 deletions src/idiomatic/polymorphism.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
minutes: 2
---

# Polymorphism

```rust
pub trait Trait {}

pub struct HasGeneric<T>(T);

pub enum Either<A, B> {
Left(A),
Right(B),
}

fn takes_generic<T: Trait>(value: &T) {}

fn takes_dyn(value: &dyn Trait) {}
```

<details>

- 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.

</details>
18 changes: 18 additions & 0 deletions src/idiomatic/polymorphism/from-oop-to-rust.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# From OOP to Rust: Composition, not Inheritance

```rust
```

<details>

- 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?

</details>
40 changes: 40 additions & 0 deletions src/idiomatic/polymorphism/from-oop-to-rust/01-inheritance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
minutes: 5
---

# Inheritance in OOP languages

```cpp
#include <iostream>
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`.
Original file line number Diff line number Diff line change
@@ -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.
}
```

<details>
- 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.

</details>
Original file line number Diff line number Diff line change
@@ -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<String>) -> 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.
34 changes: 34 additions & 0 deletions src/idiomatic/polymorphism/from-oop-to-rust/04-supertraits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
minutes: 5
---

# "Inheritance" in Rust: Supertraits

```rust
pub trait SuperTrait {}

pub trait Trait: SuperTrait {}
```

<details>
- 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.

</details>
33 changes: 33 additions & 0 deletions src/idiomatic/polymorphism/from-oop-to-rust/05-composition.md
Original file line number Diff line number Diff line change
@@ -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,
}
```

<details>
- 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.

</details>
Original file line number Diff line number Diff line change
@@ -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());
}
```

<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.

- 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<dyn Trait>`, but try to keep it homogeneous and generic-based first!

</details>
Loading
Loading