Pascal Hertleif
2017-04-30
This talk is based on my blog post Elegant Library APIs in Rust but also takes some inspiration from Brian's Rust API guidelines.
Working in libraries instead of executables, and focusing on the consumer of your API, helps you write better code.
Let's warm up with some basics that will make every library nicer.
Well-documented == Well-tested
/// Check number for awesomeness
///
/// ```rust
/// assert!(42.is_awesome());
/// ```
Always write them from a user's perspective.
Start code lines with #
to hide them in rustdoc output
Pro tip: Put setup code in a file, then # include!("src/doctest_helper.rs");
Basically: Follow Cargo's best practices
src/lib.rs
,src/main.rs
,src/bin/{name}.rs
tests/
,examples/
,benches/
#![deny(warnings, missing_docs)]
pub use specific::Thing
is your friend
Make illegal states unrepresentable
— Haskell mantra
The safest program is the program that doesn't compile
— ancient Rust proverb
(Actually: Manish on Twitter)
fn print(color: &str, text: &str) {}
print("Foobar", "blue");
fn print(color: Color, text: &str) {}
enum Color { Red, Green, CornflowerBlue }
print(Green, "lgtm");
bool
is just
enum bool { true, false }
Write your own!
enum EnvVars { Clear, Inherit }
enum DisplayStyle { Color, Monochrome }
Command::new("ls").arg("-l")
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.env_clear()
.spawn()
Builders allow you to
Builders are forward compatible:
You can change your struct's field as much as you want
Rust is a pretty concise and expressive language
...if you implement the right traits
Reduce boilerplate by converting input data
File::open("foo.txt")
Open file at this Path
(by converting the given &str
)
Implementing these traits makes it easier to work with your types
let x: IpAddress = [127, 0, 0, 1].into();
std::convert
is your friend
AsRef
: Reference to reference conversionsFrom
/Into
: Value conversionsTryFrom
/TryInto
: Fallible conversionsAll the examples we've seen so far are from std
!
Do it like std
and people will feel right at home
Implement ALL the traits!
Get "green".parse()
with FromStr:
impl FromStr for Color {
type Err = UnknownColorError;
fn from_str(s: &str) -> Result<Self, Self::Err> { }
}
Let your users iterate over your data types
For example: regex::Matches
HttpResponse::new()
.header("Foo", "1")
.header("Bar", "2")
.body("Lorem ipsum")
.header("Baz", "3")
// ^- Error, no such method
Define a type for each state
Go from one state to another by returning a different type
HttpResponse::new() // NewResponse
.header("Foo", "1") // WritingHeaders
.header("Bar", "2") // WritingHeaders
.body("Lorem ipsum") // WritingBody
.header("Baz", "3")
// ^- ERROR: no method `header` found for type `WritingBody`
More time!
More slides!
Iterators are one of Rust's superpowers
Functional programming as a zero-cost abstraction
Abstract over collections
Avoid allocations
fn foo(data: &HashMap<i32, i32>) { }
fn bar<D>(data: D) where
D: IntoIterator<Item=(i32, i32)> { }
let x: AddressBook = people.collect();
Result<YourData>
We have a list of validation criteria, like this:
[
Required,
Unique(Posts),
Max(255),
]
How can we represent this?
enum Validation {
Required,
Unique(Table),
Min(u64),
Max(u64),
}
struct Table; // somewhere
This is nice, but hard to extend.
struct Required;
struct Unique(Table);
struct Min(u64);
struct Max(u64);
And then, implement a trait like this for each one
trait Validate {
fn validate<T>(&self, data: T) -> bool;
}
This way, you can do:
use std::str::FromStr;
let validations = "max:42|required".parse()?;
Slides available at git.io/idiomatic-rust-fest