Questions about designing APIs for others
- When should my type implement
Default
? - When should my type implement
From
,Into
andTryFrom
? - How should I expose constructors?
- When should my type implement
AsRef
? - When should I implement
Copy
? - Should I have
Arc
orRc
in my API? - Should my API be thread-safe? What does that mean?
- What should I
Derive
to make my code optimally usable? - How should I think about API design, differently from C++?
See also the excellent Rust API guidelines. The document you're reading aims to provide extra hints which may be especially useful to folk coming from C++, but that's the canonical reference.
When should my type implement Default
?
Whenever you'd provide a default constructor in C++.
When should my type implement From
, Into
and TryFrom
?
You should think of these as equivalent to implicit conversions in C++. Just as with C++, if there are multiple ways to convert from your thing to another thing, don't implement these, but if there's a single obvious conversion, do.
Usually, don't implement Into
but instead implement From
.
How should I expose constructors?
See the previous two answers: where it's simple and obvious, use the standard traits to make your object behavior predictable.
If you need to go beyond that, remember you've got a couple of extra toys in Rust:
- A "constructor" could return a
Result<Self>
- Your constructors can have names, e.g.
Vec::with_capacity
,Box::pin
When should my type implement AsRef
?
If you have a type which contains another type, provide AsRef
especially
so that people can clone the inner type. It's good practice to provide explicit
versions as well (for example, String
implements AsRef<str>
but also
provides .as_str()
.)
When should I implement Copy
?
Anything that is integer-like or reference-like should be
Copy
; other things shouldn’t. - MY
When it's efficient and when it’s an API contact you're willing to uphold. - AH
Generally speaking, types which are plain-old-data can be Copy
. Anything
more nuanced with any type of state shouldn't be.
Should I have Arc
or Rc
in my API?
It’s a code smell to have reference counts in your API design. You should hide it. - TM.
If you must, you will need to decide between Rc
and Arc
- see the next
answer for some considerations. But, generally, Arc
is better practice because
it imposes fewer restrictions on your callers. Also, consider taking a look at the
Archery
crate.
Should my API be thread-safe? What does that mean?
In C++, a thread-safe API usually means that you can expect your API's consumers to use objects from multiple threads. This is difficult to make safe and therefore substantial extra engineering is required to make an API thread-safe.
In Rust, things differ:
- it's more normal to do things across multiple threads;
- you don't have to worry about your callers making mistakes here because the compiler won't let them;
- you can often rely on
Send
rather thanSync
.
You certainly shouldn't be putting a Mutex
around all your types. If your
caller attempts to use the type from multiple threads, the compiler will
simply stop them. It is the responsibility of the caller to use things
safely.
If the library has
Arc
orRc
in the APIs, it may be making choices about how you should instantiate stuff, and that’s rude. - AF
There's a reasonable chance that your API can be used in parallel threads
by virtue of Send
and Sync
being automatically derived. But - you should
think through the usage model for your API clients and ensure that's true.
use std::cell::RefCell; use std::collections::VecDeque; use std::sync::Mutex; use std::thread; // Imagine this is your library, exposing this interface to library // consumers... mod pizza_api { use std::thread; use std::time::Duration; pub struct Pizza { // automatically 'Send' _anchovies: u32, _pepperoni: u32, } pub fn make_pizza() -> Pizza { println!("cooking..."); thread::sleep(Duration::from_millis(10)); Pizza { _anchovies: 0, // yuck _pepperoni: 32, } } pub fn eat_pizza(_pizza: Pizza) { println!("yum") } } // Absolutely no changes are required to the pizza library to let // it be usable from a multithreaded context fn main() { let pizza_queue = Mutex::new(RefCell::new(VecDeque::new())); thread::scope(|s| { s.spawn(|| { let mut pizzas_eaten = 0; while pizzas_eaten < 100 { if let Some(pizza) = pizza_queue.lock().unwrap().borrow_mut().pop_front() { pizza_api::eat_pizza(pizza); pizzas_eaten += 1; } } }); s.spawn(|| { for _ in 0..100 { let pizza = pizza_api::make_pizza(); pizza_queue.lock().unwrap().borrow_mut().push_back(pizza); } }); }); }
What should I Derive
to make my code optimally usable?
The official guidelines say to be eager.
But don't overpromise:
Equality can suddenly become expensive later - don’t make types comparable unless you intend people to be able to compare instances of the type. Allowing people to pattern match on enums is usually better. - MY
Note that syn
is a rare case in that it
has so many types, and is so extensively depended upon by the rest of the Rust
ecosystem, that it avoids deriving the standard traits unless explicitly
commanded to do so via a cargo feature. This is an unusual pattern and should
not normally be followed.
How should I think about API design, differently from C++?
Make the most of the fact that everything is immutable by default. Things which are mutable should stick out. - AF
Think about things which should take self and return self. - AF
Refactoring is less expensive in Rust than C++ due to compiler safeguards, but rearchitecting is expensive in any language. Think about "one way doors" and "two way doors" in the design space: can you undo a change later?