Interface Design
There are four principles in writing idiomatic interfaces in Rust.
Interfaces should be:
- Unsurprising
- Flexible
- Obvious
- Constrained
Unsurprising Interfaces
Interfaces should be intuitive where if a user had to guess how something would work, they usually guess correctly.
Things that share similar names/prefixes should work similarly
- This includes sharing similar names with things in the stdlib.
- Users expect itermethods to take&self.
- Users expect into_innermethods to takeself.
- Users expect SomethingErrorto implementstd::error::Error.
Structs are expected to implement some traits just out of the box.
- Nearly every type should implement Debug.
- Nearly every type should be SendandSync, which are already auto-traits. If your type is NOT, this should be well-documented.
- Most types should have CloneandDefault.
- Many types should have PartialEq,PartialOrd,Hash,Eq, andOrd.
- For many types it makes sense to have serde::Serializeandserde::Deserialize.- Since serdeis a third-party crate, it is typical to expose aserdefeature in your crate that allows users to conditionally compile your library with or withoutserde.
 
- Since 
- Few types should implement Copy. Think carefully whether a user would expect your type to beCopyand whether it's even possible/feasible/a good idea.
Use blanket trait implementations for ergonomic usage.
- Even if SomeStruct: Trait, this does not automatically follow for&SomeStructor&mut SomeStruct.
- You should provide blanket implementations as appropriate for &T where T: Trait,&mut T where T: Trait, andBox<T> where T: Trait.
- For any type that can be iterated over, you should implement IntoIteratorfor both&Tand&mut Twhere applicable.
Wrapper types, e.g., NewType
- For wrapper types, it's ergonomic to have Deref<Target = Inner>andAsRef
- If your wrapper type has its own methods and uses Deref, avoid taking&selforselfor&mut self. Instead, define static methods. See, e.g.,Rc::clone(some_rc)
- In narrow cases, you'll also want to implement Borrow<T>, but note that this trait has extra semantic requirements regardingHash,Eq, andOrd.
Flexible Interfaces
Interfaces should be "flexible" in the sense that it does not make any unnecessary restrictions on usage and does not promise features that it cannot perform.
Restrictions in Rust are typically function type signatures and trait bounds.
- For example, if you need to access a string, your function could take a String, a&str, or aimpl AsRef<str>.
Promises in Rust are typically logic/behavior and return types.
- For example if you ened to return a string, your function could return a String, aCow<'_, str>, or aimpl AsRef<str>.
Generic arguments can be used to relax restrictions
- Instead of requiring a MyStructor&MyStruct, you could acceptT, which relaxes the type restriction. You can add some trait bounds toTto help it be more useful.
- You pay for making a function generic with harder to read/understand code.
- Generic code results in bigger binaries and longer compile times.
- You have the option of doing dynamic dispatch with &dyn, but doing so prevents users from opting out of the dynamic dispatch, which could be a deal-breaker in performance-sensitive code.
In general, traits should be object-safe
- If a trait is not object-safe, it cannot be made into a trait object using dyn Trait.
- If a trait method must take a generic object, you can consider adding a where Self: Sizedbound to the method which requires a concrete instance of the trait (and not throughdyn Trait).
- Object safety is part of your public interface. Adding or removing object safety is a major semantic version change.
Think carefully about whether your functions, traits, and types should own or borrow its data
- If your code needs to call methods that take selfor move the data to another thread, it must store the owned data.- It should generally make the caller provide owned data. This makes it upfront known what the cost of using the interface since the caller would have to take care of allocation.
 
- If your code doesn't need to own the data, it should just take references, (except for tiny Copytypes, like the integral types).
- The Cowtype is useful to operate on references if the data allows, or produce an owned value if you need it.
Custom Drop implementations have some pitfalls
- If you place clean-up code as part of a Dropimplementation, any errors it produces may have to be swallowed because the value is already dropped; there is no way to communicate errors to the user without panicking.
- See also: asynccode, where by the timeDropis called, the executor may also be shutting down.
- We can provide an explicit destructor, which is a function that takes ownership of selfand exposes errors using a return typeResult<_, _>.- Users can use this explicit destructor to gracefully tear down resources.
- Note that you cannot move resources out of the type inside this destructor, because Drop::dropwill still be called.
- Since Drop::droptakes&mut self, it cannot just call your explicit destructor and ignore its results, sinceDrop::dropdoesn't ownself.
- Workarounds involve unsafe, or using some combination of wrapping things inOptions and usingOption::taketo swap data out.
 
Obvious Interfaces
Interfaces should make it as easy as possible to understand and as hard as possible to use it incorrectly.
This can be enforced through:
- Good documentation
- Type system
Crash course on documentation
- Document any cases where code will do something unexpected or relies on something beyond what is dictated by the type signature.
- Include end-to-end usage examples on a crate and module level.
- Organize your documentation using modules to group together semantically related items.
- Use intra-documentation to interlink items. Meaning if Atalks aboutB, a link toBshould be right there.
- Make parts of your interface hidden with #[doc(hidden)]in order to not clutter your docs.
 
- Use intra-documentation to interlink items. Meaning if 
- Enrich your docs as much as possible wherever possible, with links to RFCs, blog posts, whitepapers, etc.
- Use #[doc(cfg(..))]to specify that items are available under certain configurations.
- Use #[doc(alias = "...")]to make types and methods discoverable by other names.
- In top-level docs, point the user to commonly used modules, features, types, traits and methods.
 
- Use 
The type system is your enforcer
- Type systems are self-documenting and misuse-resistant.
- Use semantic typingto add types which represent themeaningof a value, not just its primitive type. For example use enums instead of aboolean. Usestruct CreditCardNumberinstead ofu32.
- Use ZSTs (zero-sized types) to indicate a particular fact is true about an instance of a type. For example:
#![allow(unused)] fn main() { struct Grounded; struct Launched; struct Rocket<Stage = Grounded> { stage: std::marker::PhantomData<Stage>, } impl Default for Rocket<Grounded> {} // Methods only acceptable when rocket is still grounded impl Rocket<Grounded> { pub fn launch(self) -> Rocket<Launched> { .. } } // Methods only acceptable when rocket is in the air impl Rocket<Launched> { pub fn accelerate(&mut self) { } pub fn decelerate(&mut self) { } } // Methods acceptable in either case impl <Stage> Rocket<Stage> { pub fn color(&self) -> Color { .. } pub fn weight(&self) -> Kilograms { .. } } }
- If a function you are writing accepts a pointer argument, but only uses it if another Boolean argument is true, then it's better to combine them into anenum, one variant forMyPointer::falseand another variant forMyPointer::true(SomePointer).
- Make use of the #[must_use]annotation. The compiler issues warnings if the user's code receives an element of the annotated type and doesn't handle it.
Constrained Interfaces
Always think carefully before you make user-visible changes. Frequent backward-incompatible changes suck.
Some changes are deceptively backward incompatible, and you might not know it when you make the change.
Type Modifications are not backwards-compatible
- This involves renaming or removing a public type.
- Use visibility modifiers, i.e. pub(crate)andpub(in path)wherever possible to reduce the scope of things that a user can use.
 
- Use visibility modifiers, i.e. 
- Adding private fields is not backwards-compatible.
- Going from zero fields to one private field changes the constructor.
- Going from some public fields to another private field changes the semantics for Rust's exhaustive pattern matches because rustcsees the private fields that users cannot see.
- Make use of the #[non_exhaustive]attribute to mitigate this issue. This unfortunately makes it so that users cannot rely on exhaustive pattern matches, though.
 
Adding/Removing trait implementations is not backwards-compatible
- Users may have created their own implementations of a trait.
- A new blanket implementation will break Rust's coherence rules.
- A new implementation for an existing local trait may cause a name conflict.
- A new implementation for a foreign trait may also break coherence rules if a user already implemented it.
 
- Removing a trait implementation is a breaking change for obvious reasons: users may be relying on an implemented method.
- Implementing new traits is never a problem, though, since there is no chance for a user to have a conflicting implementation of the new trait.
- Sealed traits are great because users may only use them but not implement them, which immediately makes many breaking changes non-breaking.
- These are commonly used for derived traits, which are traits that provide blanket implementations that implement particular other traits.  Basically: impl Trait for T where T: SomeOthertrait.
- Only seal traits if it makes sense that users should not implement them.
- Sealed traits are not a feature of the Rust language, but a coding pattern. See above link.
- Sealed traits should always be documented.
 
- These are commonly used for derived traits, which are traits that provide blanket implementations that implement particular other traits.  Basically: 
Re-exports can make dependency upgrades a non-breaking change.
- Any foreign types you expose causes any change to those foreign types to be also a change to your interface.
- It's best to wrap foreign types in the NewType pattern, then expose only the parts of the foreign type that you think are useful.  Don't just blindly implement Deref!!
Auto-Traits add hidden promises to your interface for nearly every type.
- In general, these include Send,Sync,Unpin,Sized, andUnwindSafe.
- Implementations for these are automatically generated by the compiler but are also automatically not generated if a change you make makes it no longer apply.
- It is a good practice to include a simple test that checks that your type implements the traits the way you expect.
#![allow(unused)] fn main() { #[cfg(test)] mod tests { fn is_normal<T: Sized + Send + Sync + Unpin>() {} #[test] fn normal_types() { is_normal::<MyType>(); } } }