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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,10 @@
# Idiomatic Rust

- [Welcome](idiomatic/welcome.md)
- [Foundations of API Design](idiomatic/foundations-of-api-design.md)
Copy link
Collaborator

@gribozavr gribozavr Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please start this section with a slide that references https://rust-lang.github.io/api-guidelines/ . Explain that students can find many mechanical rules, written in a checklist style, in that resource. "if you are doing X, you should be doing it in this way".

The rules are like, if you're implementing a commonly-used type, you should think whether it should implement Copy, Clone, Eq etc. The rule does not provide you guidance about when to implement Copy and when not to, but it prompts the reader to think about that.

This deep dive in Comprehensive Rust will not go through that checklist type material. Tell students to explore this resource on their own later.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also reference https://microsoft.github.io/rust-guidelines/guidelines/index.html as a useful opinionated resource that explains how Microsoft approaches API design.

- [Golden Rule: Clarity & Readability](idiomatic/foundations-of-api-design/golden-rule.md)
- [Clarity: Do Provide Context](idiomatic/foundations-of-api-design/golden-rule/clarity-do-provide-context.md)
- [Readability: Consistency and Shorthand](idiomatic/foundations-of-api-design/golden-rule/readability-consistency-and-shorthands.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)
Expand Down
1 change: 1 addition & 0 deletions src/idiomatic/foundations-of-api-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Foundations of API Design
54 changes: 54 additions & 0 deletions src/idiomatic/foundations-of-api-design/golden-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Golden Rule – Callsite Clarity & Readability

A good API or a readable codebase is one that predictably follows conventions.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't how I'd introduce this rule.

I'd more or less directly say that readability at the callsite is more important than the readability of the declaration, or how the API looks on docs.rs.

The single-sentence explanation on the slide would be something like "The golden rule of API design is to optimize for clarity at the point of use (the call-site)."


```rust,editable
/// The magic function. Important for tax reasons.
fn my_magic(i: u32) -> f32 {
i as f32 + 2.
}
/// The x function. Foundational for infrastructure reasons.
fn x(i: f32) -> String {
Copy link
Collaborator

@gribozavr gribozavr Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function called x is cryptic both at the callsite and at the declaration.

This example should demonstrate a tradeoff between readability at the callsite and at the declaration.

Better examples would be method names used in From and Into traits.

Normally, method names should be verbs. But in From and Into traits the methods are named with prepositions! How come? That's because they optimize for readability at the callsite.

This is exactly where .as_*() and .into_*() naming conventions in Rust come from. These conventions don't optimize for readability at the declaration or in the doc comments, they optimize for the callsite. If they optimized for readability of the declaration, their names would include verbs like .convert_to_foo(), or .borrow_as_str(). That would optimize, for example, for searching for the word "convert" on docs.rs. But the standard library API designers have chosen a different priority. They have established the as, from and into prepositions as short and fluent naming conventions for conversions and borrowing.

(This is actually a good segue to show a brief slide which explains the standard library conventions regarding these prepositions.)

Similarly, Rust uses u32 instead of UnsignedInt32, or even uint32. These names are used so much that they are not read and understood from the first principles every time. The API designers of the standard library intended them to become standalone concepts in the mind of a Rust user.

Importantly, this is not often a tradeoff that an API designer for an "average" API must think about. Most often one can find a name that works well at the callsite and at the declaration. But, especially for foundational and frequently used methods, fluent naming is better.


This point is applicable well beyond naming (however explaining it first using method naming is how I like to do it, this maximizes the 'surprise factor' for the listener and keeps them engaged).

For example,

impl Server {
  // This looks entirely normal at the declaration.
  pub fn new(host: String, port: u16, use_tls: bool, timeout: Option<u64>) -> Self { ... }
}

// ...but the callsite is unreadable. A builder pattern would've been better.
let server = Server::new("localhost".to_string(), 8080, true, None);

Another example:

impl User {
  // Again, this looks entirely normal at the declaration.
  pub fn new(name: String) -> Self { ... }
}

// ...but if most callsites require an explicit conversion, it is a bad API!
let user = User::new("Alice".to_string());

// If we want to optimize for readability at the callsite,
// we could make `new()` take `&str`,
// or make `new` generic, taking anything that converts into `String`,
// like `new<S: Into<String>>(name: S)`.

Please also see the motivating example with HashMap in the documentation for the Borrow trait: https://doc.rust-lang.org/std/borrow/trait.Borrow.html

I honestly would not mind if you show all of these examples on the slides. In fact, since they are quite short, I don't see why we should not include all of them (on separate slides of course).

format!("{:.2}", (i / 3.).fract())
}
/// Our business logic relies on this calculation for tax reasons,
/// regulatory reasons, or critical infrastructure reasons. So if you
/// see it do be careful about changing how it's handled!
fn taxes_and_infrastructure(input: u32) -> String {
format!("{:.2}", ((input as f32 + 2.) / 3.).fract())
}
fn main() {
println!("{}", x(my_magic(128)));
println!("{}", taxes_and_infrastructure(128));
}
```

<details>

- Writing new code is often easier than reading code, so how can we make reading
code easier?

By making what is happening at a callsite of functions _as clear and readable
as possible_ before.

We can't assume a reader has read and memorized all the documentation
beforehand, we need the callsite to provide as much context as possible.
Copy link
Collaborator

@gribozavr gribozavr Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here one common trap API designers fall into is thinking that people who don't understand what the function does by looking at its name would read its documentation. This is false.

People only open documentation for a function when they are completely lost when reading a piece of code and they believe that this function is the key to understanding. When someone reads a call to a function and they believe they got a general idea of what the function kinda sorta does, they keep reading the code and don't look up the docs.


- _Calls_ to functions are going to be read far more often than the
documentation or definitions of those functions themselves.

This is true across languages, but the communities around Rust settled on
methods to keep the process of _reading code_ reliable in certain contexts.

- Ask before running: which function is more readable here, and why?

- Ask: What if we remove the "good documentation" from
`taxes_and_infrastructure`?

Without this documentation, we're only left with what's visible at the
callsite.

</details>
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
minutes: 10
---

# Clarity: Do Provide Context

Codebases are full of relationships between types, functions, inputs. Represent
them well!
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand the point this slide is trying to make. Is it trying to discuss when something should be a method vs. a free function?

I think more thorough speaker notes and a more clear call to action (than "represent them well") are necessary here.


```rust
pub struct MyType {
pub my_number: u32,
}

impl MyType {
fn add_one(&mut self) {
self.my_number += 1;
}
fn business_logic() {}
}

mod another_package {
pub fn add_one(my_type: &mut super::MyType) {
my_type.my_number += 1;
}
}

fn add_one(my_type: &mut MyType) {
my_type.my_number += 1;
}

fn main() {
let mut value = MyType { my_number: 39 };
value.add_one();
add_one(&mut value);
another_package::add_one(&mut value);
}
```

<details>

- Context clues let a reader quickly understand details about what's going on.
These can be anything from descriptive names, to if a function is a method, or
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"to if"

where that function comes from.

- Descriptive names are key, but can be subjective in highly specialized
business logic areas. Try to keep things
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete sentence


- Demo: Ask for suggestions for what the `MyType::business_logic` method does,
then ask how we might rename the method.

- Ask: What is the difference in what you assume about the source of the
function `add_one` when it's a method vs when it's a function that takes a
value, or when it's a function from another module?

- We know what it does, the name is descriptive enough.

- Potentially different authors, different packages.

While it makes sense to keep functions as methods a lot of the time, as
there's usually an "authoritative" type, there's still plenty of reasons a
function might not be a method or static method.

Note: Remember that a method is a relationship between a function and a type.

</details>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
minutes: 5
---

# Readability: Consistency and Shorthands

Be consistent in function & variable names, and use shorthands with care.

```rust,editable
// Step 1
fn do_thing() { /* Imagine something! */
}
// Step 2
fn execute_the_other_thing() {}
// Step 3
fn anthr_thng_whch_shld_b_dn() {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would appreciate less contorted examples here. Apart from the obviously bad abbrevs that drop vowels in the third example, the first two look like average fake names for slideware.

fn main() {
do_thing();
execute_the_other_thing();
anthr_thng_whch_shld_b_dn();
}
```

<details>

- Aim to be consistent in how things are named and abbreviated.

- Shorthands should be used with care, and consistent across a codebase when
used.

- This example shows three functions that all do "something." Yet each one of
them has a different naming scheme.

- Ask: Imagine what the domain should be for these three functions.

Expect a broad array of subjects, potential fallbacks:

- Ask: How should they be renamed?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not an actionable question for the audience because these are obviously fake names without semantics. We need to add a specific story for the instructor to use, and work through it in the instructor notes. (We can't rely on the instructor to come up with a compelling API design situation on the spot!)


Assume "do_thing" is the convention, so all other functions should start with
`do_<functionality>`

</details>
Loading