$30 off During Our Annual Pro Sale. View Details »

Rust API Design Learnings

Rust API Design Learnings

Armin Ronacher

February 05, 2023
Tweet

More Decks by Armin Ronacher

Other Decks in Programming

Transcript

  1. Who am I • Armin Ronacher • twitter.com/@mitsuhiko | hachyderm.io/@mitsuhiko

    • Python since time immemorial, Rust since 2012 • Python: Flask, Jinja, Werkzeug, … • Rust: Insta, MiniJinja, Console, Indicatif, Similar
  2. unnamed developer of a popular Rust crate “Sorry, I have

    no interest in making that style of coding easier. I want users to consciously choose what config they're using. I view blindly picking a default as a mistake […]”
  3. APIs are Important • A library's author's true success metrics

    are: • how successful all users are in using the API • the quality of the output that users achieve by using the API • the percentage of users making the correct choices
  4. Your User Matters • When you build a library you

    should treat it like any other thing • De fi ne success metrics • Measure yourself
  5. But we are Flying Blind • Library developers typically fl

    y blind • The only metrics we have is download stats, which mostly correlate with CI setups, and not true utilization • User frustration is often the only other form of feedback we get • We need extrapolation from user surveys and interviews • In the absence of this, personal frustration and issues is a good proxy
  6. Values: Metrics without Measuring • If we have trouble measuring,

    metrics are not useless • Metrics often express what we believe is important • Values can steer us
  7. My Values • Concise: easy to get started • Good

    Defaults: easy to get started, trivial to stay on the golden path as it changes • Small Surface Area: enable room to breath and innovate, without breaking users • Backwards compatible: avoid unnecessary churn to keep users on the golden path
  8. The Golden Path • An opinionated path for how to

    build • That path might change over time • Change requires adjustment by users • Fast change means users being left behind • Measuring success: users on the golden path (not churning, not staying on old versions, not hating the upgrade experience, not using old patterns)
  9. Use Defaults to Fight Cargo Cult • Defaults are hard

    and of two types: • Absolute defaults that cannot be changed (i32::default() -> 0) • Defaults that allow a level of fl exibility (Default Hasher: SipHash) • For defaults to allow fl exibility, care has to be taken: • Set rules and expectations about stability • Aim for some level of change
  10. Good Defaults • Default Hasher: • Hasher is documented to

    be non portable • Hasher is documented to change • No expectation around cross-version/process stability • A better hasher can be picked, all code ever written bene fi ts at once
  11. Cargo Cult • Imagine mandatory hasher • People would cargo

    cult some default
 hasher that they see elsewhere or in
 the docs. • New hasher comes around, lots of code
 stuck with the old choice.
  12. Defaults and Protocols • What if this hash becomes part

    of a protocol? • If you have an API that drives a protocol, consider that protocol to consider defaults • This approach can only be guidance, a lot of situations do not allow it.
  13. More API = More Problems • The larger the surface,

    the more of it ends up used • Less commonly used APIs have the most leaky abstractions • Inhibits future change: "does someone even use this?"
  14. Hide API Behind Common Abstractions • Developers are used to

    these patterns, they are worth exploring: • Into<T> • AsRef<T> • Careful: surface area stays large, but large bound to common and simple patterns
  15. Into • Common pairs: • Into<String> • Into<Cow<'_, T>> •

    Into<YourRuntimeType> • ToString can be sometimes an interesting alternative to Into<String>
  16. AsRef<T> • Related in Into, but for borrowing • Abstracts

    over • &String/&str/&Cow<'_, str> • &PathBuf/&Path • &[u8]/&Vec<u8>/&String/&str
  17. Monomorphization & Compile Times • Rust loves to inline •

    All those di ff erent types create
 duplicated generated code • Example: isolate conversions and
 call into shared functions to
 reduce the total amount of copied
 code.
  18. Hide the Onion but create the Onion • Good APIs

    are Layered Like Onions • Only provide the outermost layer fi rst • Keeps the inner layers fl exibility to change • Over time, consider exposing internal layers under separate stability guarantees
  19. Layer 2 and 3 • Example: CompiledTemplate is
 entirely private,

    so is the
 CodeGenerator or the parser. • It's still layered, and over time
 some functionality could be
 exposed.
  20. Explicit Exports • Hide your internal structure, re-export sensibly •

    Your folder structure does not matter to your users
  21. Explicit Fake Modules • Consider creating modules on the spot

    for utilities • For instance "insta" has utility
 functions and types that are rarely
 useful. The ones I subscribe stability
 to are re-exported under a speci fi c
 module.
  22. Public but Hidden • Sometimes stu ff needs to be

    public,
 but you don't want anyone to use it. • Common example: utility functionality
 for macros. • Here both __context and
 __context_pair! are public but hidden
  23. Traits are Tricky • Traits are super useful, but they

    are tricky • Fall into two categories: • Sealed (user should not implement) • Open (user should implement)
  24. Sealed Traits • Not really supported, doc hidden
 and hackery

    • Example in MiniJinja: want to
 abstract over types, but I don't
 really want to let the user do that.
  25. Full Seal • Uses a private zero sized marker type

    somewhere • User cannot implement or invoke as the type is private
  26. Traits are Hard to Discover • I avoid traits unless

    I know abstraction over implementations is necessary • Did you notice that BTreeMap and HashMap are not expressed via traits? • The usefulness of abstraction even for interchangeable types is sometimes unclear • You can always add traits later
  27. Debug • Put it on all public types • Consider

    it on your internal types behind a feature fl ag • Super valuable for dbg!() and co
  28. Display • Makes the type have a representation in format!()

    • It also gives it the `.to_string()` method • Certain types need it in the contract (eg: all errors) • Recommendation: avoid in most cases unless you implement a custom integer, string etc.
  29. Copy and Clone • Once granted, impossible to take away

    • Neither can be universally provided • Clone: really useful, consider adding • If you ever feel you need to take it away, consider Arc<T> internally • Copy: might inhibit future change, but really useful • Some types regrettably do not have Copy (eg: Range) and people hate it
  30. Sync and Send • I cannot give recommendations • The

    only one I have: non Send/Sync types are not that bad • Consider them seriously
  31. Lifetimes and Libraries • Try to avoid too clever setups

    • Consider "Session" abstractions where people only need to temporarily hold on to data.
  32. Borrowing to Self • Rust is really bad at this,

    sometimes you build yourself into a corner • Best tool I found to date for this is the self_cell crate • Bu ff er can be held into itself
  33. Panic vs Error • Try to avoid panics • If

    you do need to panic, consider #[track_caller]
  34. Errors Matter • Spend some time designing your errors •

    Errors deserve attention just as much as your other types • A talk all by itself, so here the basics: • Implement std::error::Error on your errors • Implement source() if you think someone might want to peak into