Elegant Library APIs in Rust
The existence of libraries with nice, user-friendly interfaces is one of the most important factors when choosing a programming language. Here are some tips on how to write libraries with nice APIs in Rust. (Many of the points also apply to other languages.)
You can also watch my talk at Rustfest 2017 about this!
Update 2017-04-27: Since writing that post, @brson of the Rust Libs Team has published a pretty comprehensive Rust API Guidelines document that includes my advice here and a lot more.
Update 2020-06-01: This post is quite a few years old by now! Most of the patterns are still valid and actively used in Rust today, but the language has also evolved quite a bit and enabled new patterns that are not discussed here. I've updated some of syntax and crate recommendations but otherwise kept the post as it was in 2016.
What makes an API elegant?
- Code using the API is easily readable thanks to obvious, self-explanatory method names.
- Guessable method names also help when using the API, so there is less need to read documentation.
- Everything has at least some documentation and a small code example.
- Users of the API need to write little boilerplate code to use it, as
- methods accept a wide range of valid input types (where conversions are obvious), and
- shortcuts to get the 'usual' stuff done quickly are available.
- Types are cleverly used to prevent logic errors, but don't get in your way too much.
- Returned errors are useful, and panic cases are clearly documented.
Techniques
Consistent names
There are a few Rust RFCs that describe the naming scheme of the standard library. You should follow them to make your library's API feel familiar for users.
- RFC 199 explains that you should use mut, move, or ref as suffixes to differentiate methods based on the mutability of their parameters.
- RFC 344 defines some interesting conventions, like
- how to refer to types in method names (e.g., &mut [T] becomes mut_slice, and mut T becomes mut_ptr),
- how to call methods that return iterators,
- that getters methods should be called field_name while setter methods should be set_field_name,
- and how to name traits: "Prefer (transitive) verbs, nouns, and then adjectives; avoid grammatical suffixes (like able)", but also "if there is a single method that is the dominant functionality of the trait, consider using the same name for the trait itself".
- RFC 430 describes some general casing conventions (tl;dr CamelCase for type-level stuff, snake_case for value-level stuff).
- RFC 445 wants you to add an Ext suffix to extension traits.
More method name conventions
In addition to what RFC 199 and RFC 344 (see above) define, there are a few more conventions around what method names to choose, which seem to not be represented in RFCs (yet). These are mostly documented in the old Rust style guidelines and in @llogiq's post Rustic Bits as well as clippy's wrong_self_convention lint. Here's a summary:
| Method name | Parameters | Notes | Examples |
|---|---|---|---|
| new | no self, usually ≥ 1 | Constructor, also cf. [Default] | Box::new, std::net::Ipv4Addr::new |
| with_... | no self, ≥ 1 | Alternative constructors | Vec::with_capacity, regex::Regex::with_size_limit |
| from_... | 1 | cf. conversion traits | String::from_utf8_lossy |
| as_... | &self | Free conversion, gives a view into data | str::as_bytes, uuid::Uuid::as_bytes |
| to_... | &self | Expensive conversion | str::to_string, std::path::Path::to_str |
| into_... | self (consumes) | Potentially expensive conversion, cf. conversion traits | std::fs::File::into_raw_fd |
| is_... | &self (or none) | Should probably return a bool | slice::is_empty, Result::is_ok, std::path::Path::is_file |
| has_... | &self (or none) | Should probably return a bool | regex_syntax::Expr::has_bytes |
Regarding new: If you can construct your type without any parameters, you should implement [Default] on it, and use that instead of new. An exception to this is new on "container" types, like Vec or HashMap, where it makes sense to initialize an empty container.
Doc tests
Write documentation with example code showing how to use your API and get automatic tests for free – Two birds, one stone. You can read more about in the documentation chapter of the official book.
rust /// assert_eq!(min( 0, 14), 0); /// assert_eq!(min( 0, -127), -127); /// assert_eq!(min(42, 666), 42); ///
To enforce that every public API item is documented, use #![deny(missing_docs)]. You might also be interested in my post suggesting conventions for formatting Rust documentation.
Don't "stringly type" your API
Coming from dynamically typed languages, you might be tempted to use strings with specific values in various places.
For example: You want a function to print some input text in a color, so you use a parameter color of type &str. That also means you expect your users to write one of the names from a limited number of color names (like ["red", "green", "blue", "light golden rod yellow"]).
This is not what you should do in Rust! If you know all possible variants beforehand, use an enum. This way, you don't need to parse/pattern match the string -- and deal with possible errors -- but can be sure that a user of your API can only ever supply valid inputs[^illegal-states].
[^illegal-states]: There is a slogan of "making illegal states unrepresentable" in other strongly typed languages. While I first heard this when talking to people about Haskell, it is also the title of this article by Ffor fun and profit, and this talk by Richard Feldman presented at elm-conf 2016.
A module full of constants
Alternatively, if you have a more complex value you want to express you can define a new struct and then define a bunch of constants with common values. If you put the constants into a public module, your users can access them using similar syntax as when using an enum variant.
Actually parsing a string with FromStr
There may still be cases where users of your API actually have strings, e.g. from reading environment variables or by taking their user input -- i.e., they didn't write (static) strings themselves in their code to give to your API (which is what we try to prevent). In those cases it makes sense to have a look at what the FromStr trait can give you, which abstracts over the concept of parsing a string into a Rust data type.
If all you want to do is map a string with an enum variant name to the right enum variant, you can adapt this macro (from this tweet; there might also be a crate for that).
Depending on your API, you could also decide to have your users deal with parsing the string. If you supply the right types and implementations, it shouldn't be difficult (but needs to be documented nonetheless).
Error handling
The official book has an awesome chapter on error handling.
There are a few crates to reduce the boilerplate needed for good error handling, e.g., anyhow (dynamic error type with methods for annotating and chaining errors), and thiserror (makes creating custom error types easy).
Public type aliases
If your internal code uses generic types with the same type parameters over and over again, it makes sense to use a type alias. If you also expose those types to your users, you should expose (and document) the type alias as well.
A common case where this is used is Result<T, E> types, where the error case (E) is fixed. For example, std::io::Result is an alias for Result<T, std::io::Error>, std::fmt::Result is an alias for Result<(), std::fmt::Error>, and serde_json::error::Result is an alias for Result<T, serde_json::error::Error>.
Use conversion traits
It's good practice to never have &String or &Vec as input parameters and instead use &str and &[T] as they allow more types to be passed in. (Basically, everything that derefs to a (string) slice).
We can apply the same idea at a more abstract level: Instead of using concrete types for input parameters, try to use generics with precise constraints. The downside of this is that the documentation will be less readable as it will be full of generics with complex constraints!
std::convert has some goodies for that:
- AsMut: A cheap, mutable reference-to-mutable reference conversion.
- AsRef: A cheap, reference-to-reference conversion.
- From: Construct Self via a conversion.
- Into: A conversion that consumes self, which may or may not be expensive.
- TryFro
Discussion in the ATmosphere