-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Foundations of API Design / Golden Rule: Clarity & Readability #2943
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Foundations of API Design |
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function called This example should demonstrate a tradeoff between readability at the callsite and at the declaration. Better examples would be method names used in 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 (This is actually a good segue to show a brief slide which explains the standard library conventions regarding these prepositions.) Similarly, Rust uses 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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() {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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> |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please also reference https://microsoft.github.io/rust-guidelines/guidelines/index.html as a useful opinionated resource that explains how Microsoft approaches API design.