1974 shaares
6 private links
6 private links
From slides http://mitsuhiko.pocoo.org/RustAPI.pdf :
APIs are Important
• A library's author's true success metrics are:
• how successful all users are in using the API
• the quality of the output that users achieve by using the API
• the percentage of users making the correct choices
Your User Matters
• When you build a library you should treat it like any other thing
• Define success metrics
• Measure yourself
• Concise: easy to get started
• Good Defaults: easy to get started, trivial to stay on the golden path as it
changes
• Small Surface Area: enable room to breath and innovate, without breaking
users
• Backwards compatible: avoid unnecessary churn to keep users on the golden
path
The Golden Path
• An opinionated path for how to build
• That path might change over time
• Change requires adjustment by users
• Fast change means users being left behind
• Measuring success: users on the golden path (not churning, not staying on
old versions, not hating the upgrade experience, not using old patterns)
Use Defaults to Fight Cargo Cult
• Defaults are hard and of two types:
• Absolute defaults that cannot be changed (i32::default() -> 0)
• Defaults that allow a level of flexibility (Default Hasher: SipHash)
• For defaults to allow flexibility, care has to be taken:
• Set rules and expectations about stability
• Aim for some level of change
Good Defaults
• Default Hasher:
• Hasher is documented to be non portable
• Hasher is documented to change
• No expectation around cross-version/process stability
• A better hasher can be picked, all code ever written benefits at once
More API = More Problems
• The larger the surface, the more of it ends up used
• Less commonly used APIs have the most leaky abstractions
• Inhibits future change: "does someone even use this?"
Hide API Behind Common Abstractions
• Developers are used to these patterns, they are worth exploring:
• Into<T>
• AsRef<T>
• Careful: surface area stays large, but large bound to common and simple
patterns
Into
• Common pairs:
• Into<String>
• Into<Cow<'_, T>>
• Into<YourRuntimeType>
• ToString can be sometimes an interesting alternative to Into<String
Into
• Common pairs:
• Into<String>
• Into<Cow<'_, T>>
• Into<YourRuntimeType>
• ToString can be sometimes an interesting alternative to Into<String
Monomorphization & Compile Times
• Rust loves to inline
• All those different types create
duplicated generated code
• Example: isolate conversions and
call into shared functions to
reduce the total amount of copied
code.
Hide the Onion but create the Onion
• Good APIs are Layered Like Onions
• Only provide the outermost layer first
• Keeps the inner layers flexibility to change
• Over time, consider exposing internal layers under separate stability
guarantees
Explicit Exports
• Hide your internal structure, re-export sensibly
• Your folder structure does not matter to your users
Explicit Fake Modules
• Consider creating modules on the spot for utilities
• For instance "insta" has utility
functions and types that are rarely
useful. The ones I subscribe stability
to are re-exported under a specific
module.
Public but Hidden
• Sometimes stuff needs to be public,
but you don't want anyone to use it.
• Common example: utility functionality
for macros.
• Here both __context and
__context_pair! are public but hidd
Traits are Tricky
• Traits are super useful, but they are tricky
• Fall into two categories:
• Sealed (user should not implement)
• Open (user should implemen
Sealed Traits
• Not really supported, doc hidden
and hackery
• Example in MiniJinja: want to
abstract over types, but I don't
really want to let the user do that
Full Seal
• Uses a private zero sized marker type somewhere
• User cannot implement or invoke as the type is private
Traits are Hard to Discover
• I avoid traits unless I know abstraction over implementations is necessary
• Did you notice that BTreeMap and HashMap are not expressed via traits?
• The usefulness of abstraction even for interchangeable types is sometimes
unclear
• You can always add traits later
Debug
• Put it on all public types
• Consider it on your internal types behind a feature flag
• Super valuable for dbg!() and co
Display
• Makes the type have a representation in format!()
• It also gives it the `.to_string()` method
• Certain types need it in the contract (eg: all errors)
• Recommendation: avoid in most cases unless you implement a custom
integer, string etc
Copy and Clone
• Once granted, impossible to take away
• Neither can be universally provided
• Clone: really useful, consider adding
• If you ever feel you need to take it away, consider Arc<T> internally
• Copy: might inhibit future change, but really useful
• Some types regrettably do not have Copy (eg: Range) and people hate it
Sync and Send
• I cannot give recommendations
• The only one I have: non Send/Sync types are not that bad
• Consider them seriously
Lifetimes and Libraries
• Try to avoid too clever setups
• Consider "Session" abstractions where people only need to temporarily hold
on to data.
Borrowing to Self
• Rust is really bad at this, sometimes you build yourself into a corner
• Best tool I found to date for this is the self_cell crate
• Buffer can be held into itself
Panic vs Error
• Try to avoid panics
• If you do need to panic, consider #[track_caller]
Errors Matter
• Spend some time designing your errors
• Errors deserve attention just as much as your other types
• A talk all by itself, so here the basics:
• Implement std::error::Error on your errors
• Implement source() if you think someone might want to peak into