Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Idiomatic Rust

Idiomatic Rust

Find the full slides and all the coverage at https://fettblog.eu/slides/idiomatic-rust/

Stefan Baumgartner

January 20, 2022
Tweet

More Decks by Stefan Baumgartner

Other Decks in Programming

Transcript

  1. About me • Product Architect at Dynatrace • Focus: Front-End

    Development, Serverless, JavaScript runtimes • Organizing Meetups and Conferences • Co-host Working Draft Podcast • fettblog.eu
  2. Tim Mans ield “Idiomatic coding follows the conventions of a

    given language. It is the most concise, convenient, and common way of accomplishing a task […]”
  3. Idiomatic Rust What we want to achieve • Use naming

    and structure that feels like Rust • Use the built-in features of Rust and its standard library well • Write code that is easy to read and follow • Create code that works well with others • Create code that is predictable, lexible, and clear
  4. Idiomatic Rust How can we achieve this? • Stick to

    naming conventions established by Rust • Use tools to follow guidelines established by the community • Rely on standard traits to interact nicely with the ecosystem • Write good Rust documentation • Use the type system! • … and more
  5. Agenda What are we learning today • Part 1: Readable

    code - Tooling, Formatting, Naming, Syntax • Part 2: Traits - Derives, Conversions • Part 3: Structs - Enums, Error Handling, Iterators • Part 4: Design Patterns - Extension traits, Builders, Typestate
  6. Formatting Rustfmt • rustc gives you warnings should you name

    something not in a Rust style • Just like Go, Rust has a formatting tool that takes care of indentation, line- breaks, imports, etc. • “Rustfmt’s style is nobody’s favourite, Rustfmt is everybody’s favourite” $ cargo fmt
  7. Clippy • Named after the infamous Microsoft Of ice “chatbot”

    (also known as Karl Klammer) • Roughly 500 lints with different levels of severity • Groups: Cargo, Complexity, Correctness, Nursery Pedantic, Perf, Style, Suspicious Rust’s linting tool
  8. Clippy Command line + VS Code • Gives you inline

    clippy lints including refactoring { "editor.formatOnSave": true, "rust-analyzer.checkOnSave.command": "clippy", "rust-analyzer.rustfmt.overrideCommand": [ "rustfmt" ] } $ cargo clippy • Clippy on the command line, rustc style errors and warnings
  9. Clippy …in Action! fn main() { let f = File::open("stairway.md").unwrap();

    let mut lines = BufReader::new(f).lines(); loop { if let Some(line) = lines.next() { let line = line.unwrap(); println!("{} ({} bytes long)", line, line.len()); } else { break; } } } warning: this loop could be written as a `while let` loop
  10. Clippy …in Action! fn main() { let f = File::open("stairway.md").unwrap();

    let mut lines = BufReader::new(f).lines(); while let Some(line) = lines.next() { let line = line.unwrap(); println!("{} ({} bytes long)", line, line.len()); } } warning: this loop could be written as a `for` loop help: try: `for line in lines`
  11. Clippy …in Action! fn main() { let f = File::open("stairway.md").unwrap();

    let lines = BufReader::new(f).lines(); for line in lines { let line = line.unwrap(); println!("{} ({} bytes long)", line, line.len()); } } ✅
  12. Rust syntax Think in Expressions let msg = if number

    > 5 { "Won!" } else { "Sorry, next time" }; let msg = match number { 0..=5 => "Won!", 6 => "Close!", _ => "Sorry, next time" }; let return_code = loop { // This is a game loop // do something if game_over { break 1 } else if quitting { break 0 } }; fn read_number() -> Option<i32> { Some(42) } Everything is an expression, until it is terminated by a semicolon Use this to execute code where you just need it
  13. Rust syntax Un-nest match expressions match read_number_from_file("number.txt") { Ok(v) =>

    println!("Your number is {}", v), Err(err) => match err { NumFromFileErr::IoError(_) => println!("Error from IO!"), NumFromFileErr::ParseError(_) => println!("Error from Parsing!"), }, }; match read_number_from_file("number.txt") { Ok(v) => println!("Your number is {}", v), Err(NumFromFileErr::IoError(_)) => println!("Error from IO!"), Err(NumFromFileErr::ParseError(_)) => println!("Error from Parsing!"), }; ✅ &
  14. Rust syntax Un-nest match expressions let a = Some(5); let

    b = Some(false); let c = match a { Some(a) => { match b { Some(b) => whatever, None => other_thing, } } None => { match b { Some(b) => another_thing, None => a_fourth_thing, } } }; & let a = Some(5); let b = Some(false); let c = match (a, b) { (Some(a), Some(b)) => whatever, (Some(a), None) => other_thing, (None, Some(b)) => another_thing, (None, None) => a_fourth_thing, }; ✅
  15. Rust syntax If let / while let if let Some((i,

    j)) = prod_min_max { // … } if let / while let allow for assignments and truthiness checks at the same time — great legibility and less code!
  16. Traits Why traits are important • Traits are the bread

    and butter of Rust • They might seem like a substitute for interfaces irst, but they are much more. In traits lies the key to Rust’s massive type system • Traits are used to make your code interoperable with the standard library and the ecosystem • Traits are used to make special syntax and operators work • Traits allow for elaborate design patterns
  17. Traits The rules of traits • You either own the

    trait, or the type • You can implement your traits for your types • You can implement foreign traits for your types • You can implement your traits for foreign types • You can’t implement foreign traits for foreign types
  18. Implement common traits Derivable traits • Copy: Values who can

    be simply copied by copying bits • Clone: Explicitly duplicate an object • PartialEq: Equality Comparing • PartialOrd: Ordering using equivalence / partial equivalence • Debug: Output via {:?} • Default: Default values
  19. Implement common traits Non-Derivable traits • Eq: Equality Comparing •

    Ord: Ordering using equivalence / partial equivalence • Display: Output via {}
  20. Default Default implementations for basic types default_impl! { (), (),

    "Returns the default value of `()`" } default_impl! { bool, false, "Returns the default value of `false`" } default_impl! { char, '\x00', "Returns the default value of `\\x00`" } default_impl! { usize, 0, "Returns the default value of `0`" } default_impl! { u8, 0, "Returns the default value of `0`" } default_impl! { u16, 0, "Returns the default value of `0`" } default_impl! { u32, 0, "Returns the default value of `0`" } default_impl! { u64, 0, "Returns the default value of `0`" } default_impl! { u128, 0, "Returns the default value of `0`" } default_impl! { isize, 0, "Returns the default value of `0`" } default_impl! { i8, 0, "Returns the default value of `0`" } default_impl! { i16, 0, "Returns the default value of `0`" } default_impl! { i32, 0, "Returns the default value of `0`" } default_impl! { i64, 0, "Returns the default value of `0`" } default_impl! { i128, 0, "Returns the default value of `0`" } default_impl! { f32, 0.0f32, "Returns the default value of `0.0`" } default_impl! { f64, 0.0f64, "Returns the default value of `0.0`" }
  21. Default How derives work • Default derives work for basic

    types • Custom types (structs, enums) need to implement Default themselves so derive works! • This again can be a derive • And so it goes down the tree until only basic types remain #[derive(Default)] struct Person { age: u16, name: Name, } struct Name { first: String, last: String, }
  22. Default How derives work • Default derives work for basic

    types • Custom types (structs, enums) need to implement Default themselves so derive works! • This again can be a derive • And so it goes down the tree until only basic types remain #[derive(Default)] struct Person { age: u16, name: Name, } struct Name { first: String, last: String, } #[derive(Default)]
  23. Default Custom Default Implementations • In a scenario like this,

    a Default implementation of 0s wouldn’t make too much sense • Instead of deriving, implement your own Default struct Fibonacci { curr: u128, next: u128, } impl Default for Fibonacci { fn default() -> Self { Fibonacci { curr: 0, next: 1 } } }
  24. Default More idols • ::new() is the convention for a

    constructor in Rust, it is not enforced, though • When you have a constructor without an argument list, implement default. Users might expect it. let fib = Fibonacci::default(); let clock = Clock { minutes, ..Default::default() }
  25. Conversions Standard conversion traits • From: Can convert from one

    type to the target type • TryFrom: Possible conversion, returns a Result • AsRef: Returns a borrow (shared reference) • AsMut: Returns a mutable borrow • Never implement Into or TryInto, you get them for free when you implement From/TryFrom
  26. Conversions Examples from the standard library • From<u16> is implemented

    for u32 because a smaller integer can always be converted to a bigger integer. • From<u32> is not implemented for u16 because the conversion may not be possible if the integer is too big. • TryFrom<u32> is implemented for u16 and returns an error if the integer is too big to it in u16.
  27. Conversions FromStr and Parse • Don’t stringly type your apps

    • If you start out with strings, use FromStr to convert strings to your types • You get a parse method for free #[derive(Debug, PartialEq)] struct Point { x: i32, y: i32, } impl FromStr for Point { type Err = ParseIntError; fn from_str(s: &str) -> Result<Self, Self::Err> { let coords: Vec<&str> = s .trim_matches(|p| p == '(' || p == ')') .split(',') .collect(); let x_fromstr = coords[0].parse::<i32>()?; let y_fromstr = coords[1].parse::<i32>()?; Ok(Point { x: x_fromstr, y: y_fromstr, }) } }
  28. A clock The main implementation #[derive(Debug, Default)] struct Clock {

    hours: i32, minutes: i32, } impl Clock { fn new(hours: i32, minutes: i32) -> Self { Self { hours, minutes }.normalize() } fn normalize(&mut self) -> Self { let mut hours = (self.hours + self.minutes / 60) % 24; let mut minutes = self.minutes % 60; if minutes < 0 { minutes += 60; hours -= 1; } if hours < 0 { hours += 24; } Self { hours, minutes } } } The Clock stores hours and minutes, a normalise method makes sure we stay within a 24 hour clock We want our clock to play well with others.
  29. A clock Pretty printing impl std::fmt::Display for Clock { fn

    fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:0>2}:{:0>2}", self.hours, self.minutes) } } • Allows for a nice output when used with format!, println!, etc. • We also know how we want to print our Clock! No iddling from the outside • Implementing Display also implements ToString automatically
  30. A clock Implementing Operators impl std::ops::Add for Clock { type

    Output = Clock; fn add(self, rhs: Self) -> Self::Output { Clock::new(self.hours + rhs.hours, self.minutes + rhs.minutes) } } impl std::ops::Add<i32> for Clock { type Output = Clock; fn add(self, rhs: i32) -> Self::Output { Clock::new(self.hours, self.minutes + rhs) } } Let’s make sure we can add any i32 to Clock The add operator for two clocks
  31. A clock Make the clock compatible • We now can

    convert any i32 into a Clock. • Implementing From gets us Into for free! impl From<i32> for Clock { fn from(val: i32) -> Clock { Clock::new(0, val) } }
  32. Conversions Into Option fn foo(lorem: &str, ipsum: Option<i32>, dolor: Option<i32>,

    sit: Option<i32>) { println!("{}", lorem); } fn main() { foo("bar", None, None, None); foo("bar", Some(42), None, None); foo("bar", Some(42), Some(1337), Some(-1)); } & fn foo<I, D, S>(lorem: &str, ipsum: I, dolor: D, sit: S) where I: Into<Option<i32>>, D: Into<Option<i32>>, S: Into<Option<i32>>, { println!("{}", lorem); } fn main() { foo("bar", None, None, None); foo("bar", 42, None, None); foo("bar", 42, 1337, -1); } ✅
  33. Enums • Rust’s enums are nominal, heterogenous, disjoint union types

    • They’re great if you want to express a inite set of variants, e.g • Use enums instead of stringly typed APIs • Use enums if you want to have clarity instead of just booleans, e.g. enum Color { Green, Blue, Red, } enum Display { Color, Monochrome } Instead of is_color_display
  34. Enums • Enums are structs! They can implement traits and

    methods just like any other struct! • Enums can carry data. You can assign values to a state! enum Room { Vacant, Occupied(Person), }
  35. Tuple structs Differentiate between basic types • Don’t just pass

    a f64 • Create a single ield tuple struct to hold the actual value • No more kilometers / miles mix-up • From/Into helps converting easily! #[derive(Default)] struct Miles(f64); #[derive(Default)] struct Kilometers(f64); fn are_we_there_yet(kilometers: Kilometers) -> bool { false } impl From<Kilometers> for Miles { fn from(km: Kilometers) -> Self { Miles(km.0 / 1.609) } } impl From<Miles> for Kilometers { fn from(miles: Miles) -> Self { Kilometers(miles.0 * 1.609) } }
  36. Make impossible states impossible • In Rust, you have no

    unde ined, null, nor do you have exceptions • You use built-in enums to model your states • Result<T, E> for results from operations that can error • Option<T> for bindings that might possibly have no value • Both types request from you to deal with the error • Either by explicitly handling all states • Or by explicitly ignoring them (e.g. unwrap)
  37. Option • Whenever a result/binding might not have any value

    • Two states: • None —> no value available (think null, but it doesn’t cause problems) • Some(x) —> value available fn divide(dividend: f32, divisor: f32) -> Option<f32> { if divisor == 0 { None } else { Some(dividend / divisor) } }
  38. Option Utility functions • Like with other enums, there are

    lots of utility functions implemented. fn read_file(filename: Option<&str>) -> Result<usize, std::io::Error> { let file = File::open(filename.unwrap_or("lyrics.md"))?; } unwrap_or sets a default value for an optional value We pass an optional ilename to our function
  39. Result • Use Result<T, E> when something might go wrong

    • Things you can do with Result: • Ignore it! • Panic! • Use a fallback • Propagate the errors • Or simply: Deal with it.
  40. Result Deal with it! fn read_username_from_file(path: &str) -> Result<String, io::Error>

    { let f = File::open(path); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(err) => Err(err), } }
  41. Result Deal with it! fn read_username_from_file(path: &str) -> Result<String, io::Error>

    { let f = File::open(path); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(err) => Err(err), } } Unwrap the value on your own
  42. Result Deal with it! fn read_username_from_file(path: &str) -> Result<String, io::Error>

    { let f = File::open(path); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(err) => Err(err), } } Unwrap the value on your own Return the error
  43. Result Ignore it fn read_username_from_file(path: &str) -> Result<String, io::Error> {

    let mut f = File::open(path).unwrap(); let mut s = String::new(); f.read_to_string(&mut s).unwrap(); Ok(s) }
  44. Result Ignore it fn read_username_from_file(path: &str) -> Result<String, io::Error> {

    let mut f = File::open(path).unwrap(); let mut s = String::new(); f.read_to_string(&mut s).unwrap(); Ok(s) } unwrap — this panics if there’s an error
  45. Result Panic! • Decide to panic the program when an

    error occurs fn read_username_from_file(path: &str) -> Result<String, io::Error> { let mut f = File::open(path).expect("Error reading file"); let mut s = String::new(); f.read_to_string(&mut s).expect("Error reading it to memory"); Ok(s) }
  46. Result Panic! • Decide to panic the program when an

    error occurs fn read_username_from_file(path: &str) -> Result<String, io::Error> { let mut f = File::open(path).expect("Error reading file"); let mut s = String::new(); f.read_to_string(&mut s).expect("Error reading it to memory"); Ok(s) }
  47. Result Use a fallback • Maybe you don’t need the

    result from your ile, maybe you already have a value … fn read_username_from_file(path: &str) -> Result<String, io::Error> { let mut f = File::open(path).expect("Error reading file"); let mut s = String::new(); f.read_to_string(&mut s).unwrap_or("admin"); Ok(s) }
  48. Result Propagate the error • If your function returns Result<T,

    E>, why not deal with the errors one level above? • Propagate the error with the question mark operator fn read_username_from_file(path: &str) -> Result<String, io::Error> { let mut f = File::open(path)?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) } fn main() { match read_username_from_file("user.txt") { Ok(username) => println!("Welcome {}", username), Err(err) => eprintln!("Whoopsie! {}", err) }; }
  49. Custom error • De ine your own enums, structs, etc.

    and implement the std::error::Error trait • You need to also implement Display and Debug #[derive(Debug)] pub struct ParseArgumentsError(String); impl std::error::Error for ParseArgumentsError {} impl Display for ParseArgumentsError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } }
  50. Propagate various errors • What if you need to propagate

    an error, but have to deal with different implementations that have no overlap? fn read_number_from_file(filename: &str) -> Result<u64, ???> { let mut file = File::open(filename)?; let mut buffer = String::new(); file.read_to_string(&mut buffer)?; let parsed: u64 = buffer.trim().parse()?; Ok(parsed) }
  51. Propagate various errors • What if you need to propagate

    an error, but have to deal with different implementations that have no overlap? fn read_number_from_file(filename: &str) -> Result<u64, ???> { let mut file = File::open(filename)?; let mut buffer = String::new(); file.read_to_string(&mut buffer)?; let parsed: u64 = buffer.trim().parse()?; Ok(parsed) } propagates std::io::Error
  52. Propagate various errors • What if you need to propagate

    an error, but have to deal with different implementations that have no overlap? fn read_number_from_file(filename: &str) -> Result<u64, ???> { let mut file = File::open(filename)?; let mut buffer = String::new(); file.read_to_string(&mut buffer)?; let parsed: u64 = buffer.trim().parse()?; Ok(parsed) } propagates std::io::Error propagates ParseIntError
  53. Iterators • Iterators are Rust’s super-power when it comes to

    anything related to collections • Iterators are lazy, until you call them in a for loop or call collect, they don’t do anything! pub fn check_isogram(candidate: &str) -> bool { let prep = candidate .to_lowercase() .chars() .filter(|c| c.is_alphabetic()) .collect::<String>(); prep.chars().unique().count() == prep.chars().count() }
  54. Iterators Obtaining integrators • Assume you have a collection c

    of type C: • c.into_iter() — Turns collection c into an Iterator i and consumes* c. Requires IntoIterator for C to be implemented. Type of item depends on what C was. 'Standardized' way to get Iterators. • c.iter() — Courtesy method some collections provide, returns borrowing Iterator, doesn't consume c. • c.iter_mut() — Same, but mutably borrowing Iterator that allow collection to be changed.
  55. Iterators Into Iterator • The IntoIterator trait is necessary to

    be compatible with for loops • All Iterator implement an IntoIterator by default • If you implement a Iterator, you only need to implement next(), all other methods depend upon next • There are different variants, e.g. ExactSizeIterator that implements methods for len if known
  56. Iterators Implementing Iterators struct Counter { count: u32, } impl

    Counter { fn new() -> Counter { Counter { count: 0 } } } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { if self.count < 5 { self.count += 1; Some(self.count) } else { None } } }
  57. Tricks with Option / Result • The Option type implements

    Iterator. You can e.g. extend a list with an option, if it has some • You can also map, etc. values inside and still keep the Option wrapper • If you have a list of Results, collecting them gives you either the irst error or a list of all possible values
  58. Task: Fibonacci sequence • Create a struct that implements the

    ibonacci sequence • Think about ways to make it idiomatic • We usually want to start with 0 and 1. Can we use Rust idioms to make sure? • Are there traits that are well suited for a sequence? • What if we want to collect a given number of ibonacci elements?
  59. Extension Traits • Extension traits extend common functionality with new

    behaviour • Instead of deriving and inheriting like in traditional OO, we can attach new functionality to structs.
  60. Extension Traits pub trait GrandResultExt { fn party(self) -> Self;

    } impl GrandResultExt for Result<String, Box<dyn Error>> { fn party(self) -> Result<String, Box<dyn Error>> { if self.is_ok() { println!("Wooohoo! '"); } self } } let fortune = Ok("32".to_string()) .party() .unwrap_or("Out of luck.".to_string());
  61. Builders • Builders are a widely used pattern in Rust

    that allows for generating structs that • Require a large number of inputs • Compound data • Optional con iguration data • Choice between several lavours • The builder pattern is especially appropriate when building involves side effects
  62. Builders • We start out like we would otherwise •

    except that we have empty collections and Options pub struct Command { program: String, args: Vec<String>, cwd: Option<String>, // etc } impl Command { pub fn new(program: String) -> Command { Command { program: program, args: Vec::new(), cwd: None, } } … }
  63. Builders • Methods change the properties of our struct, returning

    the struct itself • This is the building step impl Command { … /// Add an argument to pass to the program. pub fn arg(&mut self, arg: String) -> &mut Command { self.args.push(arg); self } /// Add multiple arguments to pass to the program. pub fn args(&mut self, args: &[String]) -> &mut Command { self.args.extend_from_slice(args); self } /// Set the working directory for the child process. pub fn current_dir(&mut self, dir: String) -> &mut Command { self.cwd = Some(dir); self } }
  64. Builders • After we are done building, we have a

    last step to get the Result • The last step returns a new type impl Command { /// Executes the command as a child process, which is returned. pub fn spawn(&self) -> io::Result<Child> { /* ... */ } }
  65. Builders • You can have one-liners as well as complex

    con iguration based on conditions Command::new("/bin/cat").arg("file.txt").spawn(); // Complex configuration let mut cmd = Command::new("/bin/ls"); cmd.arg("."); if size_sorted { cmd.arg("-S"); } cmd.spawn();
  66. Builders • Non-consuming builders work on mutable borrows and on

    the same struct. • One-liners are hard if they don’t end up with an owning last step • Complex con igurations become easy • Consuming builders transfer ownership • One-iners are easy • Complex con igurations might be hard since they require re- assignments
  67. Typestate • Back to our clock example • How would

    we differentiate between a 12-hour clock or 24-hour clock • There are several possibilities, but how can we use the type system for that?
  68. Typestate Introduce state as types • Structs without ields —>

    unit structs or markers struct TwelveHourClock; struct TwentyFourHourClock; #[derive(Debug, Default)] struct Clock<Kind = TwentyFourHourClock> { hours: i32, minutes: i32, kind: std::marker::PhantomData<Kind>, } impl Default for Clock { fn default() -> Self { Self { hours: 0, minutes: 0, kind: std::marker::PhantomData, } } } • We use a generic with a default: A 24-hour clock • PhantomData indicates that we only use kind as metadata. It evaporates at compile time
  69. Typestate Changes to our original ile • We need to

    add kind to all our functions, including the operators, normalise function, etc. impl Clock { fn new(hours: i32, minutes: i32) -> Self { Self { hours, minutes, kind: std::marker::PhantomData, } .normalize() } }
  70. Typestate Split implementations • We now can have different implementations

    for our state! impl std::fmt::Display for Clock<TwentyFourHourClock> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:0>2}:{:0>2}", self.hours, self.minutes) } } impl std::fmt::Display for Clock<TwelveHourClock> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let (hours, time_code) = if self.hours > 12 { (self.hours - 12, "p.m.") } else { (self.hours, "a.m.") }; write!(f, "{:0>2}:{:0>2} {}", hours, self.minutes, time_code) } }
  71. Typestate Conversions impl Clock<TwentyFourHourClock> { fn as_twelve_hour_clock(&self) -> Clock<TwelveHourClock> {

    Clock { hours: self.hours, minutes: self.minutes, kind: std::marker::PhantomData, } } } impl Clock<TwelveHourClock> { fn as_twenty_four_hour_clock(&self) -> Clock<TwentyFourHourClock> { Clock { hours: self.hours, minutes: self.minutes, kind: std::marker::PhantomData, } } }
  72. Task: Progress bar • We want to show the progress

    of an iterable in a for loop • Should work with bounded and unbounded iterators • Bounded progress bars should have con igurable delimiters • Think about ways to make it idiomatic • Is there a way to make sure this works with every iterator? • Can we make sure that we only set delimiters on the bounded progress bar?
  73. Resources • Idiomatic Rust: https://github.com/mre/idiomatic-rust • Elements of Rust: https://github.com/ferrous-systems/elements-of-rust

    • All the clippy lints: https://rust-lang.github.io/rust-clippy/master/ • Idiomatic Rust libraries: https://killercup.github.io/rustfest-idiomatic-libs/ index.html • Elegant APIs in Rust: https://deterministic.space/elegant-apis-in-rust.html • Rust API Guidelines: https://rust-lang.github.io/api-guidelines/about.html