From f46e8924807a722fdb80d55d2f616fe51f75cf2e Mon Sep 17 00:00:00 2001 From: tall-vase <228449146+tall-vase@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:55:07 +0000 Subject: [PATCH 1/2] Foundations of API Design chapter (#2994) Includes most of chapter 1 of foundations of API Design for Idiomatic Rust. Brought in from the gdoc. Tests have not been run locally yet, formatting has. --------- Co-authored-by: tall-vase Co-authored-by: Dmitri Gribenko --- src/SUMMARY.md | 42 ++++++++ src/idiomatic/foundations-api-design.md | 7 ++ .../meaningful-doc-comments.md | 25 +++++ .../anatomy-of-a-doc-comment.md | 84 +++++++++++++++ .../avoid-redundancy.md | 102 ++++++++++++++++++ .../meaningful-doc-comments/exercise.md | 50 +++++++++ .../library-vs-application-docs.md | 43 ++++++++ .../name-drop-signpost.md | 83 ++++++++++++++ .../meaningful-doc-comments/what-isnt-docs.md | 58 ++++++++++ .../what-why-not-how-where.md | 77 +++++++++++++ .../who-are-you-writing-for.md | 67 ++++++++++++ .../foundations-api-design/predictable-api.md | 32 ++++++ .../predictable-api/common-traits.md | 30 ++++++ .../predictable-api/common-traits/clone.md | 52 +++++++++ .../predictable-api/common-traits/copy.md | 49 +++++++++ .../predictable-api/common-traits/debug.md | 76 +++++++++++++ .../predictable-api/common-traits/display.md | 49 +++++++++ .../common-traits/from-into.md | 59 ++++++++++ .../predictable-api/common-traits/hash.md | 38 +++++++ .../common-traits/partialeq-eq.md | 55 ++++++++++ .../common-traits/partialord-ord.md | 66 ++++++++++++ .../predictable-api/common-traits/serde.md | 38 +++++++ .../common-traits/try-from-into.md | 47 ++++++++ .../predictable-api/naming-conventions.md | 20 ++++ .../naming-conventions/as-and-ref.md | 78 ++++++++++++++ .../predictable-api/naming-conventions/by.md | 61 +++++++++++ .../naming-conventions/exercise.md | 53 +++++++++ .../naming-conventions/from.md | 59 ++++++++++ .../predictable-api/naming-conventions/get.md | 31 ++++++ .../naming-conventions/into.md | 35 ++++++ .../naming-conventions/into_inner.md | 46 ++++++++ .../predictable-api/naming-conventions/is.md | 31 ++++++ .../predictable-api/naming-conventions/mut.md | 32 ++++++ .../predictable-api/naming-conventions/new.md | 31 ++++++ .../naming-conventions/push.md | 23 ++++ .../naming-conventions/raw_parts.md | 32 ++++++ .../predictable-api/naming-conventions/to.md | 60 +++++++++++ .../predictable-api/naming-conventions/try.md | 33 ++++++ .../naming-conventions/unchecked.md | 58 ++++++++++ .../naming-conventions/with-closure.md | 29 +++++ .../naming-conventions/with-constructor.md | 33 ++++++ .../naming-conventions/with-copy-setter.md | 29 +++++ .../naming-conventions/with-word.md | 37 +++++++ 43 files changed, 2040 insertions(+) create mode 100644 src/idiomatic/foundations-api-design.md create mode 100644 src/idiomatic/foundations-api-design/meaningful-doc-comments.md create mode 100644 src/idiomatic/foundations-api-design/meaningful-doc-comments/anatomy-of-a-doc-comment.md create mode 100644 src/idiomatic/foundations-api-design/meaningful-doc-comments/avoid-redundancy.md create mode 100644 src/idiomatic/foundations-api-design/meaningful-doc-comments/exercise.md create mode 100644 src/idiomatic/foundations-api-design/meaningful-doc-comments/library-vs-application-docs.md create mode 100644 src/idiomatic/foundations-api-design/meaningful-doc-comments/name-drop-signpost.md create mode 100644 src/idiomatic/foundations-api-design/meaningful-doc-comments/what-isnt-docs.md create mode 100644 src/idiomatic/foundations-api-design/meaningful-doc-comments/what-why-not-how-where.md create mode 100644 src/idiomatic/foundations-api-design/meaningful-doc-comments/who-are-you-writing-for.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/common-traits.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/common-traits/clone.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/common-traits/copy.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/common-traits/debug.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/common-traits/display.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/common-traits/from-into.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/common-traits/hash.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/common-traits/partialeq-eq.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/common-traits/partialord-ord.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/common-traits/serde.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/common-traits/try-from-into.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/as-and-ref.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/by.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/exercise.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/from.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/get.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/into.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/into_inner.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/is.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/mut.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/new.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/push.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/raw_parts.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/to.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/try.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/unchecked.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-closure.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-constructor.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-copy-setter.md create mode 100644 src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-word.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index dd6f25139ead..0121fc96d914 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -436,6 +436,48 @@ # Idiomatic Rust - [Welcome](idiomatic/welcome.md) +- [Foundations of API Design](idiomatic/foundations-api-design.md) + - [Meaningful Doc Comments](idiomatic/foundations-api-design/meaningful-doc-comments.md) + - [Who Are You Writing For?](idiomatic/foundations-api-design/meaningful-doc-comments/who-are-you-writing-for.md) + - [Library vs Application docs](idiomatic/foundations-api-design/meaningful-doc-comments/library-vs-application-docs.md) + - [Anatomy of a Doc Comment](idiomatic/foundations-api-design/meaningful-doc-comments/anatomy-of-a-doc-comment.md) + - [Name Drop and Signpost](idiomatic/foundations-api-design/meaningful-doc-comments/name-drop-signpost.md) + - [Avoid Redundancy](idiomatic/foundations-api-design/meaningful-doc-comments/avoid-redundancy.md) + - [Name and Signature are Not Enough](idiomatic/foundations-api-design/meaningful-doc-comments/what-isnt-docs.md) + - [What and Why, not How and Where](idiomatic/foundations-api-design/meaningful-doc-comments/what-why-not-how-where.md) + - [Exercise](idiomatic/foundations-api-design/meaningful-doc-comments/exercise.md) + - [Predictable API](idiomatic/foundations-api-design/predictable-api.md) + - [Naming conventions](idiomatic/foundations-api-design/predictable-api/naming-conventions.md) + - [New](idiomatic/foundations-api-design/predictable-api/naming-conventions/new.md) + - [Get](idiomatic/foundations-api-design/predictable-api/naming-conventions/get.md) + - [Push](idiomatic/foundations-api-design/predictable-api/naming-conventions/push.md) + - [Is](idiomatic/foundations-api-design/predictable-api/naming-conventions/is.md) + - [Mut](idiomatic/foundations-api-design/predictable-api/naming-conventions/mut.md) + - [With: Constructor](idiomatic/foundations-api-design/predictable-api/naming-conventions/with-constructor.md) + - [With: Copy-and-change](idiomatic/foundations-api-design/predictable-api/naming-conventions/with-copy-setter.md) + - [With: Closures](idiomatic/foundations-api-design/predictable-api/naming-conventions/with-closure.md) + - [With in normal use](idiomatic/foundations-api-design/predictable-api/naming-conventions/with-word.md) + - [Try](idiomatic/foundations-api-design/predictable-api/naming-conventions/try.md) + - [From](idiomatic/foundations-api-design/predictable-api/naming-conventions/from.md) + - [Into](idiomatic/foundations-api-design/predictable-api/naming-conventions/into.md) + - [Into inner](idiomatic/foundations-api-design/predictable-api/naming-conventions/into_inner.md) + - [By](idiomatic/foundations-api-design/predictable-api/naming-conventions/by.md) + - [Unchecked](idiomatic/foundations-api-design/predictable-api/naming-conventions/unchecked.md) + - [To](idiomatic/foundations-api-design/predictable-api/naming-conventions/to.md) + - [As and Ref](idiomatic/foundations-api-design/predictable-api/naming-conventions/as-and-ref.md) + - [Raw parts](idiomatic/foundations-api-design/predictable-api/naming-conventions/raw_parts.md) + - [Exercise](idiomatic/foundations-api-design/predictable-api/naming-conventions/exercise.md) + - [Implementing Common Traits](idiomatic/foundations-api-design/predictable-api/common-traits.md) + - [Debug](idiomatic/foundations-api-design/predictable-api/common-traits/debug.md) + - [PartialEq and Eq](idiomatic/foundations-api-design/predictable-api/common-traits/partialeq-eq.md) + - [PartialOrd and Ord](idiomatic/foundations-api-design/predictable-api/common-traits/partialord-ord.md) + - [Hash](idiomatic/foundations-api-design/predictable-api/common-traits/hash.md) + - [Clone](idiomatic/foundations-api-design/predictable-api/common-traits/clone.md) + - [Copy](idiomatic/foundations-api-design/predictable-api/common-traits/copy.md) + - [Serialize and Deserialize](idiomatic/foundations-api-design/predictable-api/common-traits/serde.md) + - [From and Into](idiomatic/foundations-api-design/predictable-api/common-traits/from-into.md) + - [TryFrom and TryInto](idiomatic/foundations-api-design/predictable-api/common-traits/try-from-into.md) + - [Display](idiomatic/foundations-api-design/predictable-api/common-traits/display.md) - [Leveraging the Type System](idiomatic/leveraging-the-type-system.md) - [Newtype Pattern](idiomatic/leveraging-the-type-system/newtype-pattern.md) - [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md) diff --git a/src/idiomatic/foundations-api-design.md b/src/idiomatic/foundations-api-design.md new file mode 100644 index 000000000000..c31a52ef740f --- /dev/null +++ b/src/idiomatic/foundations-api-design.md @@ -0,0 +1,7 @@ +--- +minutes: 2 +--- + +# Foundations of API Design + +{{%segment outline}} diff --git a/src/idiomatic/foundations-api-design/meaningful-doc-comments.md b/src/idiomatic/foundations-api-design/meaningful-doc-comments.md new file mode 100644 index 000000000000..2da5ff7c867d --- /dev/null +++ b/src/idiomatic/foundations-api-design/meaningful-doc-comments.md @@ -0,0 +1,25 @@ +--- +minutes: 5 +--- + +# Meaningful Doc Comments + +```rust,compile_fail +/// API for the client // ❌ Lacks detail +pub mod client {} + +/// Function from A to B // ❌ Redundant +fn a_to_b(a: A) -> B {...} + +/// Connects to the database. // ❌ Lacks detail +fn connect() -> Result<(), Error> {...} +``` + +
+ +- Doc comments are the most common form of documentation developers engage with. + +- Good doc comments provide information that the code, names, and types cannot, + without restating the obvious information. + +
diff --git a/src/idiomatic/foundations-api-design/meaningful-doc-comments/anatomy-of-a-doc-comment.md b/src/idiomatic/foundations-api-design/meaningful-doc-comments/anatomy-of-a-doc-comment.md new file mode 100644 index 000000000000..9c845c0445be --- /dev/null +++ b/src/idiomatic/foundations-api-design/meaningful-doc-comments/anatomy-of-a-doc-comment.md @@ -0,0 +1,84 @@ +--- +minutes: 5 +--- + +# The Anatomy of a Doc Comment + +1. A brief, one-sentence summary. +2. A more detailed explanation. +3. Special sections: code examples, panics, errors, safety preconditions. + +````rust,compile_fail +/// Parses a key-value pair from a string. +/// +/// The input string must be in the format `key=value`. Everything before the +/// first '=' is treated as the key, and everything after is the value. +/// +/// # Examples +/// +/// ``` +/// use my_crate::parse_key_value; +/// let (key, value) = parse_key_value("lang=rust").unwrap(); +/// assert_eq!(key, "lang"); +/// assert_eq!(value, "rust"); +/// ``` +/// +/// # Panics +/// +/// Panics if the input is empty. +/// +/// # Errors +/// +/// Returns a `ParseError::Malformed` if the string does not contain `=`. +/// +/// # Safety +/// +/// Triggers undefined behavior if... +unsafe fn parse_key_value(s: &str) -> Result<(String, String), ParseError> + +enum ParseError { + Empty, + Malformed, +} +```` + +
+ +- Idiomatic Rust doc comments follow a conventional structure that makes them + easier for developers to read. + +- The first line of a doc comment is a single-sentence summary of the function. + Keep it concise. `rustdoc` and other tools have a strong expectation about + that: it is used as a short summary in module-level documentation and search + results. + +- Next, you can provide a long, multi-paragraph description of the "why" and + "what" of the function. Use Markdown. + +- Finally, you can use top-level section headers to organize your content. Doc + comments commonly use `# Examples`, `# Panics`, `# Errors`, and `# Safety` as + section titles. The Rust community expects to see relevant aspects of your API + documented in these sections. + +- Rust heavily focuses on safety and correctness. Documenting behavior of your + code in case of errors is critical for writing reliable software. + +- `# Panics`: If your function may panic, you must document the specific + conditions when that might happen. Callers need to know what to avoid. + +- `# Errors`: For functions returning a `Result`, this section explains what + kind of errors can occur and under what circumstances. Callers need this + information to write robust error handling logic. + +- **Question:** Ask the class why documenting panics is so important in a + language that prefers returning `Result`. + + - **Answer:** Panics are for unrecoverable, programming errors. A library + should not panic unless a contract is violated by the caller. Documenting + these contracts is essential. + +- `# Safety` comments document safety preconditions on unsafe functions that + must be satisfied, or else undefined behavior might result. They are discussed + in detail in the Unsafe Rust deep dive. + +
diff --git a/src/idiomatic/foundations-api-design/meaningful-doc-comments/avoid-redundancy.md b/src/idiomatic/foundations-api-design/meaningful-doc-comments/avoid-redundancy.md new file mode 100644 index 000000000000..2aefebaf5558 --- /dev/null +++ b/src/idiomatic/foundations-api-design/meaningful-doc-comments/avoid-redundancy.md @@ -0,0 +1,102 @@ +--- +minutes: 15 +--- + +# Avoiding Redundancy + +Names and type signatures communicate a lot of information, don't repeat it in +comments! + +```rust,compile_fail +// Repeats name/type information. Can omit! +/// Parses an ipv4 from a str. Returns an option for failure modes. +fn parse_ip_addr_v4(input: &str) -> Option { ... } + +// Repeats information obvious from the field name. Can omit! +struct BusinessAsset { + /// The customer id. + let customer_id: u64, +} + +// Mentions the type name first thing, don't do this! +/// `ServerSynchronizer` is an orchestrator that sends local edits [...] +struct ServerSynchronizer { ... } + +// Better! Focuses on purpose. +/// Sends local edits [...] +struct ServerSynchronizer { ... } + +// Mentions the function name first thing, don't do this! +/// `sync_to_server` sends local edits [...] +fn sync_to_server(...) + +// Better! Focuses on function. +/// Sends local edits [...] +fn sync_to_server(...) +``` + +
+ +- Motivation: Documentation that merely repeats name/signature information + provides nothing new to the API user. + +Additionally, signature information may change over time without the +documentation being updated accordingly! + +- This is an understandable pattern to fall into! + + Naive approach to "always document your code," follows this advice literally + but does not follow the intent. + + Some tools might enforce documentation coverage, this kind of documentation is + an easy fix. + +- Be aware of the purpose of different modes of documentation: + + - Library code will need to be documented in ways that understand the scope of + what it is used for and the breadth of people who are trying to use it. + + - Application code has a more narrow purpose, it can afford to be more simple + and direct. + +- The name of an item is part of the documentation of that item. + + Similarly, the signature of a function is part of the documentation of that + function. + + Therefore: Some aspects of the item are already covered when you start writing + doc comments! + + Do not repeat information for the sake of an itemized list. + +- Many areas of the standard library have minimal documentation because the name + and types do give enough information. + + Rule of Thumb: What information is missing from a user's perspective? Other + than name, signature, and irrelevant details of the implementation. + +- Don't explain the basics of Rust or the standard library. Assume the reader of + doc comments has an intermediate understanding of the language itself. Focus + on documenting your API. + + For example, if your function returns `Result`, you don't need to explain how + `Result` or the question mark operators work. + +- If there is a complex topic involved with the functions and types you're + documenting, signpost to a "source of truth" if one exists such as an internal + document, a paper, a blog post etc. + +- Collaborate with Students: Go through the methods in the slide and discuss + what might be relevant to an API user. + +## More to Explore + +- The `#![warn(missing_docs)]` lint can be helpful for enforcing the existence + of doc comments, but puts a large burden on developers that could lead to + leaning onto these patterns of writing low-quality comments. + + This kind of lint should only be enabled if the people maintaining a project + can afford to keep up with its demands, and usually only for library-style + crates rather than application code. + +
diff --git a/src/idiomatic/foundations-api-design/meaningful-doc-comments/exercise.md b/src/idiomatic/foundations-api-design/meaningful-doc-comments/exercise.md new file mode 100644 index 000000000000..1d34a702b12b --- /dev/null +++ b/src/idiomatic/foundations-api-design/meaningful-doc-comments/exercise.md @@ -0,0 +1,50 @@ +--- +minutes: 10 +--- + +# Exercise: Dialog on Details + +Unnecessary details can sometimes be indicative of something that does need +documentation. + +```rust,compile_fail +/// Sorts a slice. Implemented using recursive quicksort. +fn sort_quickly(to_sort: &mut [T]) { ... } +``` + +
+ +- Consider the example here, we discussed in + [what and why, not how and where](what-why-not-how-where.md) that internal + details are unlikely relevant to someone reading documentation. + + Here we're discussing a counterexample. + +- Ask the class: Is this comment necessary for this function? + +- Narrative: Playing the part of an intermediary between the class and the + author, such as a PM, manager, etc. tell the class that the author of this + function is pushing back. + +- Ask the class: Why would an author of this kind of comment push back? + + If the class asks why the author is pushing back, do not give details yet. + +- Ask the class: Why would the caller need to know the sorting algorithm in use? + +- Narrative: "Come back" from a meeting with the original author, explain to the + class that this function is application code that is called on untrusted data + that + [could be crafted maliciously to cause quadratic behavior during sorting](https://www.cs.dartmouth.edu/~doug/mdmspe.pdf). + +- Ask the class: Now we have more detail, how should we comment this function? + + The point being implementation detail vs not depends a lot on what the public + contract is (e.g., can you supply untrusted data or not), and this requires + careful judgement. + + Consider if a comment is explaining that a for-loop is used (unnecessary + detail) or if it is explaining that the algorithms used internally have known + exploits (documentation draws attention to the wrong thing). + +
diff --git a/src/idiomatic/foundations-api-design/meaningful-doc-comments/library-vs-application-docs.md b/src/idiomatic/foundations-api-design/meaningful-doc-comments/library-vs-application-docs.md new file mode 100644 index 000000000000..854ee96d40c8 --- /dev/null +++ b/src/idiomatic/foundations-api-design/meaningful-doc-comments/library-vs-application-docs.md @@ -0,0 +1,43 @@ +--- +minutes: 10 +--- + +# Library vs application docs + +You might see elaborate documentation for fundamental APIs that repeats the +names and type signatures. Stable and highly reusable code can afford this with +a positive RoI. + +- Library code: + - has a high number of users, + - solves a whole range of related problems, + - often has stable APIs. + +- Application code is the opposite: + - few users, + - solves a specific problem, + - changes often. + +
+ +- You might have seen elaborate documentation that repeats code, looks at the + same API multiple times with many examples and case studies. Context is key: + who wrote it, for whom, and what material it is covering, and what resources + did they have. + +- Fundamental library code often has Elaborate documentation, for example, the + standard library, highly reusable frameworks like serde and tokio. Teams + responsible for this code often have appropriate resources to write and + maintain elaborate documentation. + +- Library code is often stable, so the community is going to extract a + significant benefit from elaborate documentation before it needs to be + reworked. + +- Application code has the opposite traits: it has few users, solves a specific + problem, and changes often. For application code elaborate documentation + quickly becomes outdated and misleading. It is also difficult to extract a + positive RoI from boilerplate docs even while they are up to date, because + there are only a few users. + +
diff --git a/src/idiomatic/foundations-api-design/meaningful-doc-comments/name-drop-signpost.md b/src/idiomatic/foundations-api-design/meaningful-doc-comments/name-drop-signpost.md new file mode 100644 index 000000000000..9a262abb7d58 --- /dev/null +++ b/src/idiomatic/foundations-api-design/meaningful-doc-comments/name-drop-signpost.md @@ -0,0 +1,83 @@ +--- +minutes: 15 +--- + +# Name-dropping keywords and signposting topics + +```rust +/// A parsed representation of a MARC 21 record +/// [leader](//www.loc.gov/marc/bibliographic/bdleader.html). +/// A MARC leader contains metadata that dictates how to interpret the rest +/// of the record. +pub struct Leader { + /// Determines the schema and the set of valid subsequent data fields. + /// + /// Encoded in byte 6 of the leader. + pub type_of_record: char, + + /// Indicates whether to parse relationship fields, such as a "773 Host + /// Item Entry" for an article within a larger work. + /// + /// Encoded in byte 7 of the leader. + pub bibliographic_level: char, + // ... other fields +} + +/// Parses the [leader of a MARC 21 record](https://www.loc.gov/marc/bibliographic/bdleader.html). +/// +/// The leader is encoded as a fixed-length 24-byte field, containing metadata +/// that determines the semantic interpretation of the rest of the record. +pub fn parse_leader(leader_bytes: &[u8; 24]) -> Result { + todo!() +} + +#[derive(Debug)] +pub enum MarcError {} +``` + +
+ +- Motivation: Readers of documentation will not be closely reading most of your + doc comments like they would dialogue in a novel they love. + +Users will most likely be skimming and scan-reading to find the part of the +documentation that is relevant to whatever problem they're trying to solve in +the moment. + +Once a user has found a keyword or potential signpost that's relevant to them +they will begin to search for context surrounding what is being documented. + +- Ask the class: What do you look for in documentation? Focus on the + moment-to-moment searching for information here, not general values in + documentation. + +- Name-drop keywords close to the beginning of a paragraph. + + This aids skimming and scanning, as the first few words of a paragraph stand + out the most. + + Skimming and scanning lets users quickly navigate a text, keeping keywords as + close to the beginning of a paragraph as possible lets a user determine if + they've found relevant information faster. + +- Signpost, but don't over-explain. + + Users will not necessarily have the same domain expertise as an API designer. + + If a tangential, specialist term or acronym is mentioned try to bring in + enough context such that a novice could quickly do more research. + +- Signposting often happens organically, consider a networking library that + mentions various protocols. But when it doesn't happen organically, it can be + difficult to choose what to mention. + + Rule of thumb: API developers should be asking themselves "if a novice ran + into what they are documenting, what sources would they look up and are there + any red herrings they might end up following"? + + Users should be given enough information to look up subjects on their own. + +- What we've already covered, predictability of an API including the naming + conventions, is a form of signposting. + +
diff --git a/src/idiomatic/foundations-api-design/meaningful-doc-comments/what-isnt-docs.md b/src/idiomatic/foundations-api-design/meaningful-doc-comments/what-isnt-docs.md new file mode 100644 index 000000000000..69aa5381918f --- /dev/null +++ b/src/idiomatic/foundations-api-design/meaningful-doc-comments/what-isnt-docs.md @@ -0,0 +1,58 @@ +--- +minutes: 5 +--- + +# Names and Signatures are not full documentation + +```rust,compile_fail +// bad +/// Returns a future that resolves when operation completes. +fn sync_to_server() -> Future; + +// good +/// Sends local edits to the server, overwriting concurrent edits +/// if any happened. +fn sync_to_server() -> Future; +// bad +/// Returns an error if sending the email fails. +fn send(&self, email: Email) -> Result<(), Error>; + +// good +/// Queues the email for background delivery and returns immediately. +/// Returns an error immediately if the email is malformed. +fn send(&self, email: Email) -> Result<(), Error>; +``` + +
+ +- Motivation: API designers can over-commit to the idea that a function name and + signature is enough documentation. + +It's better than nothing, but it's worse than good documentation. + +- Again, names and types are _part_ of the documentation. They are not always + the full story! + +- Consider the behavior of functions that are not covered by the name, parameter + names, or signature of that function. + + In the example on the slide it is not obvious that `sync_to_server()` could + overwrite something (leading to a data loss), so document that. + + In the email example, it is not obvious that the function can return success + and still fail to deliver the email. + +- Use comments to disambiguate. Nuanced behaviors, behaviors that users of an + API could trip up on, should be documented. + + For example, consider a remove() method on a business entity: There are many + ways to remove an entity! + + Is it removing the entity from the database? From the parent collection in + memory (unlink vs erase)? + + If it is removing the data in the database, is the data actually being + deleted, or merely marked as deleted, but still recoverable (soft vs hard + delete)? + +
diff --git a/src/idiomatic/foundations-api-design/meaningful-doc-comments/what-why-not-how-where.md b/src/idiomatic/foundations-api-design/meaningful-doc-comments/what-why-not-how-where.md new file mode 100644 index 000000000000..4340813b4ea7 --- /dev/null +++ b/src/idiomatic/foundations-api-design/meaningful-doc-comments/what-why-not-how-where.md @@ -0,0 +1,77 @@ +--- +minutes: 10 +--- + +# Why and What, not How and Where + +Avoid documenting irrelevant details that may frequently change. + +```rust,compile_fail +/// Sorts a slice. Implemented using recursive quicksort. + +fn sort_quickly(to_sort: &mut [T]) { /* ... */ +} + +// bad +/// Saves a `User` record to the Postgres database. +/// +/// This function opens a new connection and begins a transaction. It checks +/// if a user with the given ID exists with a `SELECT` query. If a user is +/// not found, performs an `INSERT`. +/// +/// # Errors +/// +/// Returns an error if any database operation fails. +pub fn save_user(user: &User) -> Result<(), db::Error> { + // ... +} + +// good +/// Atomically saves a user record. +/// +/// # Errors +/// +/// Returns a `db::Error::DuplicateUsername` error if the user (keyed by +/// `user.username` field) already exists. +pub fn save_user(user: &User) -> Result<(), db::Error> { + // ... +} +``` + +
+ +- Motivation: Users want to know the contract of the API (what is guaranteed + about this function), rather than implementation details. + +- Motivation: Doc comments that explain implementation details become outdated + faster than comments that explain the contract. + + Internal information is likely irrelevant to a user. Imagine explaining in a + doc comment for a function that you're using for loops to solve a problem, + what is the point of this information? + +- Consider the `sort_quickly` function above. Its documentation calls out that + it uses quicksort, but is this necessary? + + It could be that another sorting function is used in the future, if that were + the case then this comment would need to be updated too. This is a point of + failure in documentation. + +- It could be that the implementation is necessary to explain, but this is + likely due to whatever effects or invariants the user of that API needs to be + aware of instead. + + Focus on those effects and invariants instead of instead of the implementation + details themselves. + + Reiterate: Implementation details can and will change, so do not explain these + details. + +- Don't talk about where something is used for the sake of it. + + This is another instance where this information can become stale quickly. + +- Focus on what the function does (not how it is implemented) for a user trying + to reach this practical information as quickly as possible. + +
diff --git a/src/idiomatic/foundations-api-design/meaningful-doc-comments/who-are-you-writing-for.md b/src/idiomatic/foundations-api-design/meaningful-doc-comments/who-are-you-writing-for.md new file mode 100644 index 000000000000..d62e0567d7a1 --- /dev/null +++ b/src/idiomatic/foundations-api-design/meaningful-doc-comments/who-are-you-writing-for.md @@ -0,0 +1,67 @@ +--- +minutes: 10 +--- + +# Who are you writing for? + +Colleagues, collaborators, largely-silent API users, or just yourself? + +```rust,compile_fail +// expert writes for experts +/// Canonicalizes the MIR for the borrow checker. +/// +/// This pass ensures that all borrows conform to the NLL-Polonius constraints +/// before we proceed to MIR-to-LLVM-IR translation. +pub fn canonicalize_mir(mir: &mut Mir) { + // ... +} + +// expert writes for newcomers +/// Prepares the Mid-level IR (MIR) for borrow checking. +/// +/// The borrow checker operates on a simplified, "canonical" form of the MIR. +/// This function performs that transformation. It is a prerequisite for the +/// final stages of code generation. +/// +/// For more about Rust's intermediate representations, see the +/// [rustc-dev-guide](https://rustc-dev-guide.rust-lang.org/mir/index.html). +pub fn canonicalize_mir(mir: &mut Mir) { + // ... +} +``` + +
+ +- Background: The + [curse of knowledge](https://en.wikipedia.org/wiki/Curse_of_knowledge) is a + cognitive bias where experts assume that others have the same level of + expertise and perspective. + +- Motivation: Your reader does not have the same level of expertise and the same + perspective as you. Don't write for people like yourself, write for others. + +- Unintentionally writing for yourself can lead to people not understanding a + point you're trying to make or the concept you're trying to articulate. + +- Imagine a version of you, or others you've known, struggling to find practical + information while going through documentation. + + Keep this idea of a person in mind when thinking about what areas of a + codebase need attention for doc comments. + +- Who are you writing for? + +- Also imagine a version of you, or others you've known, who is struggling to + find the important details in winding, extensive doc comments. Don't give too + much information. + +- Always ask: Is this documentation making it difficult for the API user? Are + they able to quickly grasp what they need or find out where they could need + it? + +- Always consider: Experts also read API level documentation. Doc comments might + not be the right place to educate your audience about the basics of your + domain. In that case, signpost and name-drop. Divert people to long-form + documentation. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api.md b/src/idiomatic/foundations-api-design/predictable-api.md new file mode 100644 index 000000000000..4f78df0a7cfd --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api.md @@ -0,0 +1,32 @@ +--- +minutes: 2 +--- + +# Predictable API + +Keep your APIs predictable through naming conventions and implementing common +traits. + +```rust,compile_fail +/* What traits should this implement? */ +pub struct ApiToken(String); + +impl ApiToken { + // What should this method be called? + pub unsafe fn ____(String) -> ApiToken; +} +``` + +
+ +- A predictable API is one where a user's can make assumptions about a part of + the API based on surface-level details like names, types, and signatures. + +- We'll be looking at common naming conventions in Rust, which allow users to + search for methods that fit their needs quickly and be able to understand + existing code quickly. + +- We will also be looking at common traits that types implement, and when to + implement them for types you define. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/common-traits.md b/src/idiomatic/foundations-api-design/predictable-api/common-traits.md new file mode 100644 index 000000000000..9c25c382f465 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/common-traits.md @@ -0,0 +1,30 @@ +--- +minutes: 5 +--- + +# Common Traits to Implement + +```rust +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone /* ... */)] +pub struct MyData { + pub name: String, + pub number: usize, + pub data: [u8; 64], +} +``` + +
+- Traits are one of the most potent tools in the Rust language. The language and ecosystem expects you to use them, and so a big part of _predictability_ is what traits are implemented for a type! + +- Traits should be liberally implemented on types you author, but there are + caveats! + +- Remember, many traits have the ability to be _derived_: to have a compiler + plugin (macro) write the implementation for you! + +- Authors of ecosystem traits (like De/Serialize) have made derive + implementations for traits available to users, leading to very little + commitment needed on the developer side for implementing these kinds of + traits! + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/common-traits/clone.md b/src/idiomatic/foundations-api-design/predictable-api/common-traits/clone.md new file mode 100644 index 000000000000..c8a80c92540f --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/common-traits/clone.md @@ -0,0 +1,52 @@ +--- +minutes: 5 +--- + +# Clone + +Deep-copy a type or duplicate a smart, shareable pointer. + +Derivable: ✅ + +When to implement: If duplicating doesn't break invariants. + +```rust +// pub trait Clone: Sized { +// // Required method +// fn clone(&self) -> Self; +// +// // Provided methods omitted +// } + +use std::collections::BTreeSet; +use std::rc::Rc; + +#[derive(Clone)] +pub struct LotsOfData { + string: String, + vec: Vec, + set: BTreeSet, +} + +let lots_of_data = LotsOfData { + string: "String".to_string(), + vec: vec![1; 255], + set: BTreeSet::from_iter([1, 2, 3, 4, 5, 6, 7, 8]), +}; + +let lots_of_data_cloned = lots_of_data.clone(); + +let reference_counted = Rc::new(lots_of_data); +// Copies the reference-counted pointer, not the value. +let reference_copied = reference_counted.clone(); +``` + +
+ +- "Deep copy" a value, or in the case of reference counting pointers like + `Rc`/`Arc` create a new instance of that pointer. + +- When to not implement/derive: For types that, to maintain an invariant, the + value should not be duplicated. We'll touch on this later in Idiomatic Rust. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/common-traits/copy.md b/src/idiomatic/foundations-api-design/predictable-api/common-traits/copy.md new file mode 100644 index 000000000000..09331affe680 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/common-traits/copy.md @@ -0,0 +1,49 @@ +--- +minutes: 10 +--- + +# Copy + +Like `Clone`, but indicates the type is can be bitwise copied. + +Derivable: ✅ + +When to implement: If possible, but with caveats. + +```rust +// Copy is just a marker trait with Clone as a supertrait. +// pub trait Copy: Clone { } + +#[derive(Clone, Copy)] +pub struct Copyable(u8, u16, u32, u64); +``` + +
+- Clone represents a deep clone, and so does copy, but copy suggests to the compiler that a value can be copied bitwise. + +- When not to implement/derive: If you do not want to implicitly create copies + when dereferencing values of a type, do not implement this trait. + + Copy enables implicit duplication, so be careful about what types you're + implementing this on. + +- Ask the class: Can a type with heap data (`Vec`, `BTreeMap`, `Rc`, etc.) be + copy? Should it be? + + It both cannot and should not, this is a misuse of this trait. + + Bitwise copying on these types would mean types with heap data would no longer + have exclusive ownership of a pointer, breaking the invariants usually upheld + by Rust and its ecosystem. + + Multiple `Vec`s would point to the same data in memory. Adding and removing + data would only update individual `Vec`s length and capacity values. The same + for `BTreeMap`. + + Bitwise copying of `Rc`s would not update the reference counting value within + the pointers, meaning there could be two instances of a `Rc` value that + believe themselves to be the only `Rc` for that pointer. Once one of them is + destroyed, the reference count will become 0 on one of them and the inner + value dropped despite there being another `Rc` still alive. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/common-traits/debug.md b/src/idiomatic/foundations-api-design/predictable-api/common-traits/debug.md new file mode 100644 index 000000000000..3c7924114dac --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/common-traits/debug.md @@ -0,0 +1,76 @@ +--- +minutes: 5 +--- + +# Debug + +"Write to string" trait, for debug purposes. + +Derivable: ✅ + +When to implement: Almost always + +```rust +// pub trait Debug { +// fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>; +// } + +#[derive(Debug)] +pub struct Date { + day: u8, + month: u8, + year: i64, +} + +#[derive(Debug)] +pub struct User { + name: String, + date_of_birth: Date, +} + +pub struct PlainTextPassword { + password: String, + hint: String, +} + +impl std::fmt::Debug for PlainTextPassword { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PlainTextPassword") + .field("hint", &self.hint) + .field("password", &"[omitted]") + .finish() + } +} + +fn main() { + let user = User { + name: "Alice".to_string(), + date_of_birth: Date { day: 31, month: 10, year: 2002 }, + }; + + println!("{user:?}"); + println!( + "{:?}", + PlainTextPassword { + password: "Password123".to_string(), + hint: "Used it for years".to_string() + } + ); +} +``` + +
+- Provides trivial "write to string" functionality. + +- Formatting for _debug information_ for programmers during , not appearance or + serialization. + +- Allows for use of `{:?}` and `{#?}` interpolation in string formatting macros. + +- When to not derive/implement: If a struct holds sensitive data, investigate if + you should implement Debug for it. + + If Debug is needed, consider manually implementing Debug rather than deriving + it. Omit the sensitive data from the implementation. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/common-traits/display.md b/src/idiomatic/foundations-api-design/predictable-api/common-traits/display.md new file mode 100644 index 000000000000..926cede7a87d --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/common-traits/display.md @@ -0,0 +1,49 @@ +--- +minutes: 5 +--- + +# Display + +"Write to string" trait, prioritizing readability for an end user. + +Derivable: ❌, without crates like `derive_more`. + +When to implement: As-needed, for errors and other types that an end-user will +see. + +```rust +#[derive(Debug)] +pub enum NetworkError { + HttpCode(u16), + WhaleBitTheUnderseaCable, +} + +impl std::fmt::Display for NetworkError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NetworkError::HttpCode(code) => write!(f, "HTTP Error code {code}"), + NetworkError::WhaleBitTheUnderseaCable => { + write!(f, "Whale attack detected – call Ishmael") + } + } + } +} + +impl std::error::Error for NetworkError {} +``` + +
+- A trait similar to `Debug`, but with a focus on end-user readability. + +- Prerequisite for the `Error` trait. + + If implementing for an error type, focus on providing a descriptive error for + users and programmers other than you. + +- Same security considerations as Debug, consider the ways that sensitive data + could be exposed in UI or logs. + +- Types that implement `Display` automatically have `ToString` implemented for + them. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/common-traits/from-into.md b/src/idiomatic/foundations-api-design/predictable-api/common-traits/from-into.md new file mode 100644 index 000000000000..ea1b19d669cf --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/common-traits/from-into.md @@ -0,0 +1,59 @@ +--- +minutes: 5 +--- + +# From & Into + +Conversion from one type to another. + +Derivable: ❌, without crates like `derive_more`. + +When to implement: As-needed and convenient. + +```rust +pub struct ObviousImplementation(String); + +impl From for ObviousImplementation { + fn from(value: String) -> Self { + ObviousImplementation(value) + } +} + +impl From<&str> for ObviousImplementation { + fn from(value: &str) -> Self { + ObviousImplementation(value.to_owned()) + } +} + +fn main() { + // From String + let obvious1 = ObviousImplementation::from("Hello, obvious!".to_string()); + // From &str + let obvious2 = ObviousImplementation::from("Hello, obvious!"); + // A From implementation implies an Into implementation, &str.into() -> + // ObviousImplementation + let obvious3: ObviousImplementation = "Hello, implementation!".into(); +} +``` + +
+- Provides conversion functionality to types. + +- The two traits exist to express different areas you'll find conversion in + codebases. + +- `From` provides a constructor-style function, whereas into provides a method + on an existing value. + +- Prefer writing `From` implementations for a type you're authoring instead + of `Into`. + + The `Into` trait is implemented for any type that implements `From` + automatically. + + `Into` is preferred as a trait bound for arguments to functions for clarity of + intent for what the function can take. + + `T: Into` has clearer intent than `String: From`. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/common-traits/hash.md b/src/idiomatic/foundations-api-design/predictable-api/common-traits/hash.md new file mode 100644 index 000000000000..a87ebc881586 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/common-traits/hash.md @@ -0,0 +1,38 @@ +--- +minutes: 2 +--- + +# Hash + +Performing a hash on a type. + +Derivable: ✅ + +When to implement: Almost always. + +```rust +// pub trait Hash { +// // Required method +// fn hash(&self, state: &mut H) +// where H: Hasher; +// +// // Provided method +// fn hash_slice(data: &[Self], state: &mut H) +// where H: Hasher, +// Self: Sized { ... } +// } + +#[derive(Hash)] +pub struct User { + id: u32, + name: String, + friends: Vec, +} +``` + +
+- Allows a type to be used in hash algorithms. + +- Most commonly used with data structures like `HashMap`. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/common-traits/partialeq-eq.md b/src/idiomatic/foundations-api-design/predictable-api/common-traits/partialeq-eq.md new file mode 100644 index 000000000000..92cb2b4e5329 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/common-traits/partialeq-eq.md @@ -0,0 +1,55 @@ +--- +minutes: 10 +--- + +PartialEq and Eq + +Partial equality & Total equality. + +Derivable: ✅ + +When to implement: Almost always. + +```rust +// pub trait PartialEq +//{ +// // Required method +// fn eq(&self, other: &Rhs) -> bool; +// +// // Provided method +// fn ne(&self, other: &Rhs) -> bool { ... } +// } +// +// pub trait Eq: PartialEq { } + +#[derive(PartialEq, Eq)] +pub struct User { name: String, favorite_number: i32 } + +let alice = User { name: "alice".to_string(), favorite_number: 1_000_042 }; +let bob = User { name: "bob".to_string(), favorite_number: 42 }; + +dbg!(alice == alice); +dbg!(alice == bob); +``` + +
+- Equality-related methods. If a type implements `PartialEq`/`Eq` then you can use the `==` operator with that type. + +- A type can't implement `Eq` without implementing `PartialEq`. + +- Reminder: Partial means "there are invalid members of this set for this + function." + + This doesn't mean that equality will panic, or that it returns a result, just + that there may be values that may not behave as you expect equality to behave. + + For example, with floating point values `NaN` is an outlier: `NaN == NaN` is + false, despite bitwise equality. + + `PartialEq` exists to separate types like f32/f64 from types with Total + Equality. + +- You can implement `PartialEq` between different types, but this is mostly + useful for reference/smart pointer types. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/common-traits/partialord-ord.md b/src/idiomatic/foundations-api-design/predictable-api/common-traits/partialord-ord.md new file mode 100644 index 000000000000..c3ff7271d8d9 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/common-traits/partialord-ord.md @@ -0,0 +1,66 @@ +--- +minutes: 10 +--- + +# PartialOrd and Ord + +Partial ordering & Total ordering. + +Derivable: ✅ + +When to implement: Almost always. + +```rust +// pub trait PartialOrd: PartialEq +// { +// // Required method +// fn partial_cmp(&self, other: &Rhs) -> Option; +// +// /* Provided methods omitted */ +// } +// pub trait Ord: Eq + PartialOrd { +// // Required method +// fn cmp(&self, other: &Self) -> Ordering; +// +// /* Provided methods omitted */ +// } + +#[derive(PartialEq, PartialOrd)] +pub struct Partially(f32); + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +pub struct Totally { + id: u32, + name: String, +} +``` + +
+- Comparison-related methods. If a type implements `PartialOrd`/`Ord` then you can use comparison operators (`<`, `<=`, `>`, `>=`) with that type. + +`Ord` gives access to `min`, `max`, and `clamp` methods. + +- When derived, compares things in the order they are defined. + + For enums this means each variant is considered "greater than" the last as + they are written. + + For structs this means fields are compared as they are written, so `id` fields + are compared before `name` fields in `Totally`. + +- Prerequisites: `PartialEq` for `PartialOrd`, `Eq` for `Ord`. + + To implement `Ord`, a type must also implement `PartialEq`, `Eq`, and + `PartialOrd`. + +- Like with `PartialEq` and `Eq`, a type cannot implement `Ord` without + implementing `PartialOrd`. + + Like those equality traits, `PartialOrd` exists to separate types with + non-total ordering (particularly floating-point numbers) from types with total + ordering. + +- Used for sorting/searching algorithms and maintaining the ordering of + `BTreeMap`/`BTreeSet` style data types. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/common-traits/serde.md b/src/idiomatic/foundations-api-design/predictable-api/common-traits/serde.md new file mode 100644 index 000000000000..c37ca609e9e1 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/common-traits/serde.md @@ -0,0 +1,38 @@ +--- +minutes: 5 +--- + +Serialize/Deserialize style traits + +Crates like `serde` can implement serialization automatically. + +Derivable: ✅ + +When to implement: Almost always. + +```rust,compile_fail +#[derive(Serialize, Deserialize)] +struct ExtraData { + fav_color: String, + name_of_dog: String, +} + +#[derive(Serialize, Deserialize)] +struct Data { + name: String, + age: usize, + extra_data: ExtraData, +} +``` + +
+- Provides serialization and deserialization functionality for a type. + +- When not to implement: If a type contains sensitive data that should not be + erroneously saved to disk or sent over a network, consider not implementing + Serialize/Deserialize for that type. + + Shares security concerns with `Debug`, but given serialization is often used + in networking there can be higher stakes. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/common-traits/try-from-into.md b/src/idiomatic/foundations-api-design/predictable-api/common-traits/try-from-into.md new file mode 100644 index 000000000000..ed41c4f080bd --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/common-traits/try-from-into.md @@ -0,0 +1,47 @@ +--- +minutes: 5 +--- + +# TryFrom/TryInto + +Fallible conversion from one type to another. + +Derivable: ❌ + +When to implement: As-needed. + +```rust +#[derive(Debug)] +pub struct InvalidNumber; + +#[derive(Debug)] +pub struct DivisibleByTwo(usize); + +impl TryFrom for DivisibleByTwo { + type Error = InvalidNumber; + fn try_from(value: usize) -> Result { + if value.rem_euclid(2) == 0 { + Ok(DivisibleByTwo(value)) + } else { + Err(InvalidNumber) + } + } +} + +fn main() { + let success: Result = 4.try_into(); + dbg!(success); + let fail: Result = 5.try_into(); + dbg!(fail); +} +``` + +
+- Provides conversion that can fail, returning a result type. + +- Like `From`/`Into`, prefer implementing `TryFrom` for types rather than + `TryInto`. + +- Implementations can specify what the error type of the `Result`. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions.md new file mode 100644 index 000000000000..91e306686e3b --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions.md @@ -0,0 +1,20 @@ +--- +minutes: 2 +--- + +# Naming Conventions + +
+- One core component of readability and predictability is the way function names are composed. + +A formal and consistently-applied naming convention lets developers treat names +like a domain-specific language and quickly understand the functionality and use +cases of a method. + +Rust's community developed naming conventions early, making them mostly +consistent in places like the standard library. + +- Here we'll learn common components of rust method names, giving examples from + the standard library and some context to go with them. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/as-and-ref.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/as-and-ref.md new file mode 100644 index 000000000000..e316c8306572 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/as-and-ref.md @@ -0,0 +1,78 @@ +--- +minutes: 5 +--- + +# `as_` and `_ref`: reference conversions + +`as` is a prefix for methods that convert references. `ref` is a suffix (but +prefer `as`.) + +`as` methods borrow out the primary piece of data contained in `&self`. + +Most commonly return references, but can also return a custom borrowing type or +an unsafe pointer. + +```rust,compile_fail +impl Rc { + fn as_ptr(&self) -> *const T; + + // Very common on container types, see how it's also on Option. + fn as_ref(&self) -> &T; +} + +impl Option { + fn as_ref(&self) -> Option<&T>; + // Slices can be empty! So this is 0 or 1 elements. + fn as_slice(&self) -> &[T]; +} + +impl OwnedFd { + // Covered later. + fn as_fd(&'a self) -> BorrowedFd<'a>; +} +``` + +
+ +- Method that returns a borrow of the primary piece of contained data. + +- The borrowing relationship is most often straightforward: the return value is + a reference that borrows `self`. + +- Borrowing can also be subtle, and merely implied. + + - The returned value could be a custom borrowing type, fore example, + `BorrowedFd` borrows `OwnedFd` through an explicit lifetime. + + - We cover custom borrowing types later in this deep dive, + [PhantomData: OwnedFd & BorrowedFd](../../../leveraging-the-type-system/borrow-checker-invariants/phantomdata-04-borrowedfd.md). + + - The returned value could borrow `self` only logically, for example, + `as_ptr()` methods return an unsafe pointer. The borrow checker does not + track borrowing for pointers. + +- The type implementing an "as" method should contain one primary piece of data + that is being borrowed out. + + - The "as" naming convention does not work if the data type is an aggregate of + many fields without an obvious primary one. Think about the call site: + + ```rust,compile_fail + my_vec.as_ptr() // OK + my_person.as_first_name() // does not read right, don't use "as_" + my_person.first_name() // OK + ``` + + - If you want to have two getters that you need to distinguish, one that + returns first name by value, and another one that returns it by reference, + use `_ref` suffix: + + ```rust,compile_fail + impl Person { + fn first_name(&self) -> String + fn first_name_ref() -> &str + fn first_name_mut() -> &mut String + } + ``` + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/by.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/by.md new file mode 100644 index 000000000000..7a980900c650 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/by.md @@ -0,0 +1,61 @@ +--- +minutes: 2 +--- + +# `by`: custom comparator or projection + +Component for methods that take a custom projection or comparison function. + +```rust,compile_fail +impl [T] { + // Simplified + fn sort_by(&mut self, compare: impl FnMut(&T, &T) -> Ordering); + + // Uses a predicate to determine what items end up in non-overlapping chunks. + fn chunk_by_mut bool>( + &mut self, + pred: F, + ) -> ChunkByMut<'_, T, F>; +} + +trait Iterator { + // Provided method of Iterator. Simplified. + fn min_by( + self, + compare: impl FnMut(&Self::Item, &Self::Item) -> Ordering, + ) -> Option; +} +``` + +
+- Method will take a comparison or projection function. + +A projection function here being a function that, given a reference to a value +that exists in the data structure, will compute a value to perform the principle +computation with. + +Methods like `sort_by_key` allow us to sort by _the hash function I've passed to +the method_ or sort by _this specific field of the data in the slice_. + +For example, if you have a slice of values of some data structure you might want +to sort them by a field of that data structure, or even a hash value of that +data. + +`sort_by` takes a comparator function directly. + +- Most often seen in methods that sort or otherwise manipulate a slice with a + custom sort or comparison function rather than by the `Ord` implementation of + the type itself. + +- Sometimes the "by" preposition is simply a preposition. + + "by", like some other name components, may end up in a method name for normal + linguistic reasons rather than holding specific naming convention semantic + weight. + + - [`Read::by_ref()`](https://doc.rust-lang.org/std/io/trait.Read.html#method.by_ref) + + - [`Iterator::advance_by()`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.advance_by) + iterator method (nightly feature) + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/exercise.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/exercise.md new file mode 100644 index 000000000000..b48ff6224055 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/exercise.md @@ -0,0 +1,53 @@ +--- +minutes: 10 +--- + +# Exercise + +1. What do these names imply they do? +2. What should we name these signatures? + +```rust,compile_fail +// What are the types of these methods? +Option::is_some // ? +slice::get // ? +slice::get_unchecked_mut // ? +Option::as_ref // ? +str::from_utf8_unchecked_mut // ? +Rc::get_mut // ? +Vec::dedup_by_key // ? + +// What should we name methods with these types? +fn ____(String) -> Self; +fn ____(&self) -> Option<&InnerType>; // details for InnerType do not matter. +fn ____(self, String) -> Self; +fn ____(&mut self) -> Option<&mut InnerType>; +``` + +
+ +- Go through the methods in the example with the class and discuss what the + types of the functions should be. + +- Go through the unnamed methods and brainstorm what names those methods should + have. + + Answers for missing types: + - `Option::is_some(&self) -> bool` + - `slice::get(&self /* &[T] */, usize) -> Option<&T>` + - `slice::get_unchecked_mut(&self /* &[T] */, usize) -> &T` (unsafe and + simplified) + - `Option::as_ref(&self /* &Option */) -> Option<&T>` + - `str::from_utf8_unchecked_mut(v: &mut [u8]) -> &mut str` (unsafe) + - `Rc::get_mut(&mut self /* &mut Rc */) -> Option<&mut T>` (simplified) + - `Vec::dedup_by_key(&mut self /* &mut Vec */, key: impl FnMut(&mut T) -> K)` + (simplified) + + Answers for missing names: + - `fn from_string(String) -> Self` + - `fn inner(&self) -> Option<&InnerType>` or `as_ref`, depending on context + - `fn with_string(self, String) -> Self` + - `fn inner_mut(&mut self) -> Option<&mut InnerType>` or `as_ref_mut`, + depending on context + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/from.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/from.md new file mode 100644 index 000000000000..cd9dc2501286 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/from.md @@ -0,0 +1,59 @@ +--- +minutes: 2 +--- + +# `from` + +A constructor function, strongly implying "type conversion". + +```rust,compile_fail +impl CStr { + unsafe fn from_ptr<'a>(ptr: *const i8) -> &'a CStr; +} + +impl Duration { + fn from_days(days: u64) -> Duration; +} + +impl Vec { + fn from_raw_parts(ptr: *mut T, length: usize, capacity: usize) -> Vec; +} + +impl i32 { + fn from_ascii(src: &[u8]) -> Result; +} + +impl u32 { + fn from_le_bytes(bytes: [u8; 4]) -> u32; +} +``` + +
+- Prefix for constructor-style, `From`-trait-style functions. + +- These functions can take multiple arguments, but usually imply the user is + doing more of the work than a usual constructor would. + + `new` is still preferred for most constructor-style functions, the implication + for `from` is transformation of one data type to another. + +- Ask: Without looking at the standard library documentation, what would the + argument type of `u32::from_be` be? + + Answer guidance: we already see `u32::from_le_bytes` on the slide, it takes a + slice of bytes. So from_le must be simpler, taking not bytes. Think about the + contrast between `u32` and `be`. The argument must be a big-endian `u32`! + + Follow-up question: How about `str::from_utf8`? + + Answer guidance: `str` vs `utf8`. The argument can't be a `str` because every + `str` is valid UTF-8. So what is the simplest way to provide UTF-8 data? A + slice of bytes. + + Follow-up: Why not `str::from_utf8_bytes`? + + Answer: It could be in theory. However, the "omit needless words" principle + applies, the word "bytes" would merely repeat the obvious - could a UTF-8 + sequence ever be non-bytes? + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/get.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/get.md new file mode 100644 index 000000000000..2e4bdc1cd890 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/get.md @@ -0,0 +1,31 @@ +--- +minutes: 2 +--- + +# `get`: Borrow an Element + +Getting an element from a collection or container. + +```rust,compile_fail +impl Vec { + fn get(&self, index: usize) -> Option<&T> {...} +} + +impl OnceCell { + fn get(&self) -> Option<&T> {...} +} +``` + +
+- Gets are trivial, they get a value! + +Immutable by default, for the most part. + +Should not panic. May return an option or result, depending on the framework. + +- Not for fields! + + For private fields you don't want users to have direct, assign a method with a + more descriptive name (or the same name as the field) is preferred. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/into.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/into.md new file mode 100644 index 000000000000..c60d37a4ac13 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/into.md @@ -0,0 +1,35 @@ +--- +minutes: 2 +--- + +# `into` + +- Prefix for methods that convert `self` into another type. + +Consumes `self`, returns an owned value. + +```rust,compile_fail +impl Vec { + fn into_parts(self) -> (NonNull, usize, usize); +} + +impl Cell { + fn into_inner(self) -> T; +} +``` + +
+- Prefix for a function that consumes an owned value and transforms it into a value of another type. + +Not reinterpret cast! The data can be rearranged, reallocated, changed in any +way, including losing information. + +- corollary to `From` + +- `into_iter` consumes a collection (like a vec, or a btreeset, or a hashmap) + and produces an iterator over owned values, unlike `iter` and `iter_mut` which + produce iterators over reference values. + +- Ask the class: what will `Vec::into_raw_parts` do? + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/into_inner.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/into_inner.md new file mode 100644 index 000000000000..3d5511b7898d --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/into_inner.md @@ -0,0 +1,46 @@ +--- +minutes: 2 +--- + +# Aside: `into_inner` + +Special case of `into`: for exclusive pointer types or newtypes, extract the +internal value. + +```rust,compile_fail +pub struct Wrapper(T); + +impl Wrapper { + fn into_inner(self) -> T; +} + +pub struct NonZeroU32(u32); + +impl NonZeroU32 { + fn into_inner(self) -> u32; +} + +impl Cell { + fn into_inner(self) -> T; +} +``` + +
+ +- `into_inner` is a method usually found on newtypes: types whose main purpose + is to wrap around an existing type and be semantically distinct from other + uses of that inner type. + +This kind of method is also found on types like `Cell`, which exclusively own +the internal data. + +The purpose of this kind of method is to consume the "wrapper" type and return +the "contained" value. + +- When defining a type with exactly one field, consider if it makes sense to + implement an `into_inner` method that consumes `self` and returns the field as + an owned value. + + Don't write a method like this if more fields will be added in the future. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/is.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/is.md new file mode 100644 index 000000000000..3164359fb4b5 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/is.md @@ -0,0 +1,31 @@ +--- +minutes: 2 +--- + +# `is_[condition]`: Boolean Check + +Check a condition about a datatype. + +```rust,compile_fail +impl Vec { + is_empty(&self) -> bool; +} + +impl f32 { + is_nan(self) -> bool; +} + +impl u32 { + is_power_of_two(self) -> bool; +} +``` + +
+- A boolean condition on a value. + +- `is` prefix is preferred over methods with `not` in the name. + + There are no instances of `is_not_` in standard library methods, just use + `!value.is_[condition]`. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/mut.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/mut.md new file mode 100644 index 000000000000..38a96b55062c --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/mut.md @@ -0,0 +1,32 @@ +--- +minutes: 2 +--- + +# `[method]_mut`: Mutable reference access + +Suffix for access-style methods. + +```rust,compile_fail +impl Vec { + // Simplified + fn get_mut(&mut self, usize) -> Option<&T>; +} + +impl [T] { + // Simplified + fn iter_mut(&mut self) -> impl Iterator; +} + +impl str { + fn from_utf8_mut(v: &mut [u8]) -> Result<&mut str, Utf8Error>; +} +``` + +
+- Mut for Mutability + +- Suffix that signifies the method gives access to a mutable reference. + + Requires mutable access to the value you're calling this method on. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/new.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/new.md new file mode 100644 index 000000000000..014d4b6eee1e --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/new.md @@ -0,0 +1,31 @@ +--- +minutes: 1 +--- + +# `new`: Constructor functions + +Rust does not have a `new` keyword, instead `new` is a common prefix or whole +method name. + +```rust,compile_fail +impl Vec { + // Creates an empty vec. + fn new() -> Vec; +} + +impl Box { + fn new(T) -> Box; +} +``` + +
+ +- There's no `new` keyword for rust to initialize a new value, only functions + you call or values you directly populate. + + `new` is conventional for the "default" constructor function for a type. It + holds no special syntactic meaning. + + This is sometimes a prefix, it sometimes takes arguments. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/push.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/push.md new file mode 100644 index 000000000000..33ec8336f733 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/push.md @@ -0,0 +1,23 @@ +--- +minutes: 2 +--- + +# `push` + +Common on array-like structures. + +```rust,compile_fail +impl Vec { + fn push(&mut self, value: T); +} + +impl VecDeque { + fn push_back(&mut self, value: T); + fn push_front(&mut self, value: T); +} +``` + +
+- Modifies a sequential collection by adding an element. + +- Takes `self` by mutable reference. diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/raw_parts.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/raw_parts.md new file mode 100644 index 000000000000..0118ba8f5409 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/raw_parts.md @@ -0,0 +1,32 @@ +--- +minutes: 2 +--- + +# `raw_parts` + +Peeling back safe abstractions on heap data. + +```rust,compile_fail +impl Vec { + // Note how this is an unsafe function + unsafe fn from_raw_parts(ptr: *mut T, length: usize, capacity: usize) -> Vec; + + fn into_raw_parts(self) -> (*mut T, usize, usize); +} +``` + +
+ +- `raw_parts` denotes methods that construct items from or decompose items into + underlying pointer data and its relevant layout information (capacity, etc.). + +- These kinds of methods can be marked as `unsafe` if constructing new values as + trust is placed on the user to avoid conditions that might lead to undefined + behavior. + + Such a case might be passing a pointer of `sizeof T * 10` to + `Vec::from_raw_parts` but also passing `20` as the capacity argument, which + would lead to writing or accessing values 10 through 19 in the vector being + undefined behavior. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/to.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/to.md new file mode 100644 index 000000000000..2a019eca44bd --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/to.md @@ -0,0 +1,60 @@ +--- +minutes: 2 +--- + +# `to`: Non-consuming Conversion + +Prefix to a function that takes a borrowed value and creates an owned value + +```rust,compile_fail +impl str { + // &str is not consumed. + fn to_owned(&str) -> String; + + fn to_uppercase(&self) -> String; +} + +impl u32 { + // take an owned self because `u32` implements `Copy` + to_be(self) -> u32; +} +``` + +
+- Methods that create a new owned value without consuming `self`, and imply a type conversion, are named starting with `to`. + +- This is not a borrow checker escape hatch, or an instance of unsafe code. A + new value is created, the original data is left alone. + +- Methods that start with "to" return a different type, and strongly imply a + non-trivial type conversion, or even a data transformation. For example, + `str::to_uppercase`. + +- "to" methods most commonly take `&self`. However they can take `self` by value + if the type implements `Copy`: this also ensures that the conversion method + call does not consume `self`. + +- If you simply want to define a method that takes `&self` and returns an owned + value of the same type, implement the `Clone` trait. + +Example: to_uppercase creates a version of a string with all uppercase letters. + +- If you want to define a method that consumes the source value, use the "into" + naming pattern. + +- Also seen in functions that convert the endianness of primitives, or copy and + expose the value of a newtype. + +## More to Explore + +- Ask the class: What's the difference between `to_owned` and `into_owned`? + + Answer: `to_owned` appears on reference values like `&str`, whereas + `into_owned` appears on owned values that hold reference types, like `Cow` + (copy-on-write). + + Types like `Cow` can be owned while containing references that are borrowed, + so the owned value of `Cow` is consumed to create an owned value of the + reference type it was holding onto. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/try.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/try.md new file mode 100644 index 000000000000..5f8e45fce32a --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/try.md @@ -0,0 +1,33 @@ +--- +minutes: 2 +--- + +# `try_[method]`: Fallible methods with Specific Errors + +Prefix for fallible methods that return a `Result`. + +```rust,compile_fail +impl TryFrom for u32 { + type Error = TryFromIntError; + fn try_from(value: i32) -> Result; +} + +impl Receiver { + try_recv(&self) -> Result; +} +``` + +
+- Prefix for methods that can fail, returning a `Result`. + +- `TryFrom` is a `From`-like trait for types whose single-value constructors + might fail in some way. + +- Ask: Why aren't `Vec::get` and other similar methods called `try_get`? + + Methods are named `get` if they return a reference to an existing value and + return an `Option` instead of `Result` because there is only one failure mode. + For example, only "index out of bounds" for `Vec::get`, and "key does not + exist" for `HashMap::get`. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/unchecked.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/unchecked.md new file mode 100644 index 000000000000..b6637b8d8227 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/unchecked.md @@ -0,0 +1,58 @@ +--- +minutes: 5 +--- + +# `unchecked`: Unsafe + +`unchecked` distinguishes the unsafe function in a safe/unsafe pair. + +Don't add "unchecked" to the name of every unsafe function. + +```rust,compile_fail +impl NonNull { + // A checked version of the constructor, `None` on null. + fn new(ptr: *mut T) -> Option> + + // Unchecked constructor, you can violate the non-null invariant! + unsafe fn new_unchecked(ptr: *mut T) -> NonNull +} + +impl Vec { + // Panics on OOB, old API design. + fn split_at(&self, mid: usize) -> (&[T], &[T]) + + // Newer method, returns `None` if mid > len + fn split_at_checked(&self, mid: usize) -> Option<(&[T], &[T])> + + // Unchecked split function, splitting out of bounds is undefined behavior! + unsafe fn split_at_unchecked(&self, mid: usize) -> (&[T], &[T]) +} +``` + +
+- Sometimes we need to define a pair of functions that have very similar behavior, but one is safe, and the other one is unsafe. + +- Please take the Unsafe Rust deep dive if you want to learn more about unsafe + code. Briefly, unsafe functions transfer the responsibility for memory safety + from the compiler to the programmer. If misused, they can trigger undefined + behavior. + +- Rust does not overload functions on safety, so we use different names for the + functions in the pair. To make the names predictable for users, we use a + naming convention. + +- The safe function gets the short name. We add "unchecked" to the name of the + unsafe function. + +- We don't add "unchecked" to the name of every unsafe function. + + - In Rust we don't need a naming convention to highlight the danger of unsafe + code at the callsite: Rust already requires the caller to write an + `unsafe {}` block. This is different from other languages that don't have + unsafe blocks, for example, Swift naming convention is to add the word + "unsafe" to the type and function names. + + - We only use this naming convention when we want to provide a function pair, + and therefore must use different names. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-closure.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-closure.md new file mode 100644 index 000000000000..b93dfa8853d6 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-closure.md @@ -0,0 +1,29 @@ +--- +minutes: 2 +--- + +# `with`: Working with Closures + +`with` as in "do X, but with this specific way of computing things." + +```rust,compile_fail +impl Vec { + // Simplified. If the resize is larger than the current vec size, use the + // closure to populate elements. + pub fn resize_with(&mut self, new_len: usize, f: impl FnMut() -> T); +} + +mod iter { + // Create an infinite, lazy iterator using a closure. + pub fn repeat_with A>(repeater: F) -> RepeatWith; +} +``` + +
+ +- `with` can appear as a suffix to communicate there is a specific function or + closure that can be used instead of a "sensible default" for a computation. + + Similar to [`by`](./by.md). + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-constructor.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-constructor.md new file mode 100644 index 000000000000..3e92fdfe86f1 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-constructor.md @@ -0,0 +1,33 @@ +--- +minutes: 2 +--- + +# `with` as constructor + +`with` as a constructor sets one value among a type while using default values +for the rest. + +`with` as in "`` with specific setting." + +```rust,compile_fail +impl Vec { + // Initializes memory for at least N elements, len is still 0. + fn with_capacity(capacity: usize) -> Vec; +} +``` + +
+ +- `with` can appear as a constructor prefix, most commonly when initializing + heap memory for container types. + + In this case, it's distinct from `new` constructors because it specifies the + value for something that is not usually cared about by API users. + +- Ask the class: Why not `from_capacity`? + + Answer: `Vec::with_capacity` as a method call scans well as creating a "Vec + with capacity". Consider how `Vec::new_capacity` or `Vec::from_capacity` scan + when written down, they do not communicate what's going on well. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-copy-setter.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-copy-setter.md new file mode 100644 index 000000000000..dedd2cd35a89 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-copy-setter.md @@ -0,0 +1,29 @@ +--- +minutes: 2 +--- + +# `with` as copy-and-set + +`with` appears when a value is being copied, but also changed in a specific way. + +`with` as in "like ``, but with something different." + +```rust,compile_fail +impl Path { + // Simplified. "/home/me/mortgage.pdf".with_extension("mov") => + // "/home/me/mortgage.mov" + fn with_extension(&self, ext: &OsStr) -> PathBuf; +} +``` + +
+ +- `with` can be used for methods that copy a value, but then change a specific + part of that value. + + In the example here, `with_extension` copies the data of a `&Path` into a new + `PathBuf`, but changes the extension to something else. + + The original `Path` is unchanged. + +
diff --git a/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-word.md b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-word.md new file mode 100644 index 000000000000..98c9bcc4bac2 --- /dev/null +++ b/src/idiomatic/foundations-api-design/predictable-api/naming-conventions/with-word.md @@ -0,0 +1,37 @@ +--- +minutes: 2 +--- + +# `with` in normal use + +Sometimes a `with` is just a `with`. + +`with` when used in common English contexts. + +```rust,compile_fail +// impl block for slices +impl [T] { + // A condition, but doesn't start with `is`, and uses `with` as a normal word. + fn starts_with(&self, &[T]) -> bool; +} +``` + +
+ +- Name fragments are not hard rules, they are guidance. Sometimes a method's + name will include words that break its pattern. + +- In this example with have `starts_with`, which is a boolean condition that + does not start with "is" and is suffixed by "with". + + If naming conventions were to be treated as hard rules, this would fail as a + case. + + This is a good name for understanding what is going on at the callsite. We end + up writing `.starts_with()` which scans well for authors + and readers of code. + +- Remember: the point of naming conventions is predictability, and how + predictability is in service of callsite clarity and readability. + +
From f51bf778a8f9df5aaf470d1097c60dfa38fd2948 Mon Sep 17 00:00:00 2001 From: tall-vase <228449146+tall-vase@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:57:18 +0000 Subject: [PATCH 2/2] Polymorphism chapter (#2995) Includes most of chapter 4, Polymorphism, for Idiomatic Rust. Brought in from the gdoc. Tests have not been run locally yet, formatting has. --------- Co-authored-by: tall-vase --- src/SUMMARY.md | 29 +++++++ src/idiomatic/polymorphism.md | 30 +++++++ .../polymorphism/from-oop-to-rust.md | 27 +++++++ .../from-oop-to-rust/composition.md | 36 +++++++++ .../dynamic-dispatch/any-trait.md | 49 +++++++++++ .../dynamic-dispatch/dyn-compatible.md | 54 +++++++++++++ .../dynamic-dispatch/dyn-trait.md | 39 +++++++++ .../dynamic-dispatch/dyn-vs-generics.md | 43 ++++++++++ .../dynamic-dispatch/heterogeneous.md | 43 ++++++++++ .../dynamic-dispatch/limits.md | 41 ++++++++++ .../dynamic-dispatch/pitfalls.md | 68 ++++++++++++++++ .../from-oop-to-rust/inheritance.md | 45 +++++++++++ .../from-oop-to-rust/problem-solving.md | 81 +++++++++++++++++++ .../from-oop-to-rust/sealed-traits.md | 53 ++++++++++++ .../from-oop-to-rust/sealing-with-enums.md | 48 +++++++++++ .../from-oop-to-rust/sticking-with-traits.md | 39 +++++++++ .../from-oop-to-rust/supertraits.md | 36 +++++++++ .../from-oop-to-rust/switch-perspective.md | 57 +++++++++++++ .../from-oop-to-rust/why-no-inheritance.md | 73 +++++++++++++++++ src/idiomatic/polymorphism/refresher.md | 22 +++++ .../polymorphism/refresher/blanket-impls.md | 59 ++++++++++++++ .../refresher/conditional-methods.md | 47 +++++++++++ .../polymorphism/refresher/default-impls.md | 44 ++++++++++ .../polymorphism/refresher/deriving-traits.md | 49 +++++++++++ .../refresher/monomorphization.md | 45 +++++++++++ .../polymorphism/refresher/orphan-rule.md | 69 ++++++++++++++++ src/idiomatic/polymorphism/refresher/sized.md | 39 +++++++++ .../polymorphism/refresher/supertraits.md | 49 +++++++++++ .../polymorphism/refresher/trait-bounds.md | 39 +++++++++ .../polymorphism/refresher/traits.md | 63 +++++++++++++++ 30 files changed, 1416 insertions(+) create mode 100644 src/idiomatic/polymorphism.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/composition.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/any-trait.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-compatible.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-trait.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-vs-generics.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/heterogeneous.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/limits.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/pitfalls.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/inheritance.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/problem-solving.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/sealed-traits.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/sealing-with-enums.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/sticking-with-traits.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/supertraits.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/switch-perspective.md create mode 100644 src/idiomatic/polymorphism/from-oop-to-rust/why-no-inheritance.md create mode 100644 src/idiomatic/polymorphism/refresher.md create mode 100644 src/idiomatic/polymorphism/refresher/blanket-impls.md create mode 100644 src/idiomatic/polymorphism/refresher/conditional-methods.md create mode 100644 src/idiomatic/polymorphism/refresher/default-impls.md create mode 100644 src/idiomatic/polymorphism/refresher/deriving-traits.md create mode 100644 src/idiomatic/polymorphism/refresher/monomorphization.md create mode 100644 src/idiomatic/polymorphism/refresher/orphan-rule.md create mode 100644 src/idiomatic/polymorphism/refresher/sized.md create mode 100644 src/idiomatic/polymorphism/refresher/supertraits.md create mode 100644 src/idiomatic/polymorphism/refresher/trait-bounds.md create mode 100644 src/idiomatic/polymorphism/refresher/traits.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 0121fc96d914..51e5c3c2a932 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -521,6 +521,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/traits.md) + - [Trait Bounds](idiomatic/polymorphism/refresher/trait-bounds.md) + - [Deriving Traits](idiomatic/polymorphism/refresher/deriving-traits.md) + - [Default Implementations](idiomatic/polymorphism/refresher/default-impls.md) + - [Supertraits](idiomatic/polymorphism/refresher/supertraits.md) + - [Blanket Implementations](idiomatic/polymorphism/refresher/blanket-impls.md) + - [Conditional Methods](idiomatic/polymorphism/refresher/conditional-methods.md) + - [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) + - [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) + - [Problem solving: Break Down the Problem](idiomatic/polymorphism/from-oop-to-rust/problem-solving.md) --- diff --git a/src/idiomatic/polymorphism.md b/src/idiomatic/polymorphism.md new file mode 100644 index 000000000000..f94cbae78c3f --- /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 somewhat different from other popular 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..c172bee1870e --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust.md @@ -0,0 +1,27 @@ +--- +minutes: 2 +--- + +# From OOP to Rust: Composition, Not Inheritance + +- 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 you represent heterogeneous collections in rust? + +
+ +- In this section we'll be looking at how to move from thinking about + polymorphic problem solving with types in OOP languages like java, C++ etc. to + Rust's trait-based approach to Polymorphism. + +- There will be differences, but there are also plenty of areas in common + – especially with modern standards of OOP development. Remember to keep an + open mind. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/composition.md b/src/idiomatic/polymorphism/from-oop-to-rust/composition.md new file mode 100644 index 000000000000..3324813d1974 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/composition.md @@ -0,0 +1,36 @@ +--- +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/dynamic-dispatch/any-trait.md b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/any-trait.md new file mode 100644 index 000000000000..bfcaafbd400c --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/any-trait.md @@ -0,0 +1,49 @@ +--- +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/dynamic-dispatch/dyn-compatible.md b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-compatible.md new file mode 100644 index 000000000000..f81f3d34e0a2 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-compatible.md @@ -0,0 +1,54 @@ +--- +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/dynamic-dispatch/dyn-trait.md b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-trait.md new file mode 100644 index 000000000000..bd5120ab79e0 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-trait.md @@ -0,0 +1,39 @@ +--- +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/dynamic-dispatch/dyn-vs-generics.md b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-vs-generics.md new file mode 100644 index 000000000000..e99606dfd3d8 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/dyn-vs-generics.md @@ -0,0 +1,43 @@ +--- +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/dynamic-dispatch/heterogeneous.md b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/heterogeneous.md new file mode 100644 index 000000000000..be6da4bc81d2 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/heterogeneous.md @@ -0,0 +1,43 @@ +--- +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 + 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/dynamic-dispatch/limits.md b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/limits.md new file mode 100644 index 000000000000..46624fd63b18 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/limits.md @@ -0,0 +1,41 @@ +--- +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/dynamic-dispatch/pitfalls.md b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/pitfalls.md new file mode 100644 index 000000000000..eb83e51fa0fc --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/dynamic-dispatch/pitfalls.md @@ -0,0 +1,68 @@ +--- +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/inheritance.md b/src/idiomatic/polymorphism/from-oop-to-rust/inheritance.md new file mode 100644 index 000000000000..815052b2cb8c --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/inheritance.md @@ -0,0 +1,45 @@ +--- +minutes: 5 +--- + +# Inheritance in OOP languages + +```cpp +#include +using namespace std; + +// Base class +class Vehicle { +public: + void accelerate() { } + void brake() { } +}; + +// Inheriting class +class Car : public Vehicle { +public: + void honk() { } +}; + +int main() { + Car myCar; // Create a Car object + myCar.accelerate(); // Inherited method + myCar.honk(); // Car's own method + myCar.brake(); // Inherited method + return 0; +} +``` + +
+ +- This should be a short reminder for students about what inheritance is in + other languages. + +- 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/problem-solving.md b/src/idiomatic/polymorphism/from-oop-to-rust/problem-solving.md new file mode 100644 index 000000000000..44dff3be8367 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/problem-solving.md @@ -0,0 +1,81 @@ +--- +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 ") + } + + fn line(&self, start: [f32; 2], end: [f32; 2]) { /* ... */ + } +} + +// Question: What's a good API for users? + +pub trait Draw { + fn draw(&self, surface: &mut T); +} + +pub struct Rect { + start: [f32; 2], + end: [f32; 2], +} + +impl Draw for Rect { + 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/from-oop-to-rust/sealed-traits.md b/src/idiomatic/polymorphism/from-oop-to-rust/sealed-traits.md new file mode 100644 index 000000000000..741ca22ed05b --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/sealed-traits.md @@ -0,0 +1,53 @@ +--- +minutes: 5 +--- + +# 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 monomorphized functions for each type. + +
diff --git a/src/idiomatic/polymorphism/from-oop-to-rust/sealing-with-enums.md b/src/idiomatic/polymorphism/from-oop-to-rust/sealing-with-enums.md new file mode 100644 index 000000000000..0257d8ab1d48 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/sealing-with-enums.md @@ -0,0 +1,48 @@ +--- +minutes: 5 +--- + +# Sealing with Enums + +```rust +use std::collections::BTreeMap; +pub enum GetSource { + WebUrl(String), + BytesMap(BTreeMap>), +} + +impl GetSource { + fn get(&self, url: &str) -> Option<&Vec> { + match self { + Self::WebUrl(source) => unimplemented!(), + Self::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/sticking-with-traits.md b/src/idiomatic/polymorphism/from-oop-to-rust/sticking-with-traits.md new file mode 100644 index 000000000000..f517ae7669ae --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/sticking-with-traits.md @@ -0,0 +1,39 @@ +--- +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/supertraits.md b/src/idiomatic/polymorphism/from-oop-to-rust/supertraits.md new file mode 100644 index 000000000000..af66868568c7 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/supertraits.md @@ -0,0 +1,36 @@ +--- +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/switch-perspective.md b/src/idiomatic/polymorphism/from-oop-to-rust/switch-perspective.md new file mode 100644 index 000000000000..49627b322a66 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/switch-perspective.md @@ -0,0 +1,57 @@ +--- +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/why-no-inheritance.md b/src/idiomatic/polymorphism/from-oop-to-rust/why-no-inheritance.md new file mode 100644 index 000000000000..134db3e636c4 --- /dev/null +++ b/src/idiomatic/polymorphism/from-oop-to-rust/why-no-inheritance.md @@ -0,0 +1,73 @@ +--- +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/refresher.md b/src/idiomatic/polymorphism/refresher.md new file mode 100644 index 000000000000..518d053c4edf --- /dev/null +++ b/src/idiomatic/polymorphism/refresher.md @@ -0,0 +1,22 @@ +--- +minutes: 2 +--- + +# Refresher + +Basic features of Rust's generics and polymorphism. + +```rust,compile_fail +pub struct HasGenerics(...); + +pub fn uses_traits(input: T) {...} + +pub trait TraitBounds: Clone {...} +``` + +
+ +- In this section we'll be going through the core concepts of Rust's approach to + polymorphism, the things you'll run into the most in day-to-day usage. + +
diff --git a/src/idiomatic/polymorphism/refresher/blanket-impls.md b/src/idiomatic/polymorphism/refresher/blanket-impls.md new file mode 100644 index 000000000000..286e23ac98a6 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/blanket-impls.md @@ -0,0 +1,59 @@ +--- +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/conditional-methods.md b/src/idiomatic/polymorphism/refresher/conditional-methods.md new file mode 100644 index 000000000000..e97e8e9d0d23 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/conditional-methods.md @@ -0,0 +1,47 @@ +--- +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/default-impls.md b/src/idiomatic/polymorphism/refresher/default-impls.md new file mode 100644 index 000000000000..06e74b712c81 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/default-impls.md @@ -0,0 +1,44 @@ +--- +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 in terms of other methods available, such + as other methods in the trait or methods of a supertrait. + +- 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/deriving-traits.md b/src/idiomatic/polymorphism/refresher/deriving-traits.md new file mode 100644 index 000000000000..621c7d4a0979 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/deriving-traits.md @@ -0,0 +1,49 @@ +--- +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/monomorphization.md b/src/idiomatic/polymorphism/refresher/monomorphization.md new file mode 100644 index 000000000000..5550da6eb3e6 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/monomorphization.md @@ -0,0 +1,45 @@ +--- +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/refresher/orphan-rule.md b/src/idiomatic/polymorphism/refresher/orphan-rule.md new file mode 100644 index 000000000000..c28bfae7dfd3 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/orphan-rule.md @@ -0,0 +1,69 @@ +--- +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/sized.md b/src/idiomatic/polymorphism/refresher/sized.md new file mode 100644 index 000000000000..9bec328a873f --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/sized.md @@ -0,0 +1,39 @@ +--- +minutes: 2 +--- + +# Statically Sized and Dynamically Sized Types + +```rust +use std::fmt::Debug; + +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/supertraits.md b/src/idiomatic/polymorphism/refresher/supertraits.md new file mode 100644 index 000000000000..8a7ed769ecd6 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/supertraits.md @@ -0,0 +1,49 @@ +--- +minutes: 5 +--- + +# Supertraits / Trait Dependencies + +Traits can be extended by new traits. + +```rust +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/trait-bounds.md b/src/idiomatic/polymorphism/refresher/trait-bounds.md new file mode 100644 index 000000000000..bb6db4fe529d --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/trait-bounds.md @@ -0,0 +1,39 @@ +--- +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/traits.md b/src/idiomatic/polymorphism/refresher/traits.md new file mode 100644 index 000000000000..e0f23d93b3a9 --- /dev/null +++ b/src/idiomatic/polymorphism/refresher/traits.md @@ -0,0 +1,63 @@ +--- +minutes: 10 +--- + +# Traits, Protocols, Interfaces + +```rust +trait Receiver { + fn send(&self, message: &str); +} + +struct Email { + email: String, +} + +impl Receiver for Email { + fn send(&self, message: &str) { + println!("Email to {}: {}", self.email, message); + } +} + +struct ChatId { + uuid: [u8; 16], +} + +impl Receiver 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 + +