Idiomatic Rust Libraries

Pascal Hertleif

2017-04-30

Hi, I'm Pascal Hertleif

This talk is based on my blog post Elegant Library APIs in Rust but also takes some inspiration from Brian's Rust API guidelines.

Goals for our libraries

Easy to use

  • Quick to get started with
  • Easy to use correctly
  • Flexible and performant

Easy to maintain

  • Common structures, so code is easy to grasp
  • New contributor friendly == Future-self friendly
  • Well-tested

Working in libraries instead of executables, and focusing on the consumer of your API, helps you write better code.

Andrew Hobden

Some easy things first

Let's warm up with some basics that will make every library nicer.

Doc tests

Well-documented == Well-tested

/// Check number for awesomeness
///
/// ```rust
/// assert!(42.is_awesome());
/// ```

Nice, concise doc tests

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");

Directories and files

Basically: Follow Cargo's best practices

  • src/lib.rs,
  • src/main.rs,
  • src/bin/{name}.rs
  • tests/,
  • examples/,
  • benches/

Get more compiler errors!

  • #![deny(warnings, missing_docs)]
  • Use Clippy

Keep the API surface small

  • Less to learn
  • Less to maintain
  • Less opportunity to introduce breaking changes

pub use specific::Thing is your friend

Use the type system, Luke

Make illegal states unrepresentable

— Haskell mantra

The safest program is the program that doesn't compile

— ancient Rust proverb

(Actually: Manish on Twitter)

Avoid stringly-typed APIs

fn print(color: &str, text: &str) {}

print("Foobar", "blue");

fn print(color: Color, text: &str) {}

enum Color { Red, Green, CornflowerBlue }

print(Green, "lgtm");

Avoid lots of booleans

bool is just

enum bool { true, false }

Write your own!

enum EnvVars { Clear, Inherit }

enum DisplayStyle { Color, Monochrome }

Builders

Command::new("ls").arg("-l")
    .stdin(Stdio::null())
    .stdout(Stdio::inherit())
    .env_clear()
    .spawn()

Builders allow you to

  • validate and convert parameters implicitly
  • use default values
  • keep your internal structure hidden

Builders are forward compatible:

You can change your struct's field as much as you want

Make typical conversions easy

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 conversions
  • From/Into: Value conversions
  • TryFrom/TryInto: Fallible conversions

What would std do?

All 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!

  • Debug, (Partial)Ord, (Partial)Eq, Hash
  • Display, Error
  • Default
  • (Serde's Serialize + Deserialize)

Goodies: Parse Strings

Get "green".parse() with FromStr:

impl FromStr for Color {
    type Err = UnknownColorError;

    fn from_str(s: &str) -> Result<Self, Self::Err> { }
}

Goodies: Implement Iterator

Let your users iterate over your data types

For example: regex::Matches

Session types

HttpResponse::new()
.header("Foo", "1")
.header("Bar", "2")
.body("Lorem ipsum")
.header("Baz", "3")
// ^- Error, no such method

Implementing Session Types

Define a type for each state

Go from one state to another by returning a different type

Annotated example

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`

Questions

Do we have some more time?

More time!

More slides!

Iterators

Iterators are one of Rust's superpowers

Functional programming as a zero-cost abstraction

Iterator as input

Abstract over collections

Avoid allocations

fn foo(data: &HashMap<i32, i32>) { }

fn bar<D>(data: D) where
  D: IntoIterator<Item=(i32, i32)> { }

Construct types using FromIterator

let x: AddressBook = people.collect();

Extension traits

  • Implement a trait for types like Result<YourData>
  • Implement a trait generically for other traits

Example: Validations

We have a list of validation criteria, like this:

[
  Required,
  Unique(Posts),
  Max(255),
]

How can we represent this?

Using enums

enum Validation {
  Required,
  Unique(Table),
  Min(u64),
  Max(u64),
}

struct Table; // somewhere

This is nice, but hard to extend.

Use tuple/unit structs

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()?;

Thanks!

Any questions?

Slides available at git.io/idiomatic-rust-fest