Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. Idiomatic Rust
    fettblog.eu - oida.dev - rust-training.eu - rust-linz.at
    Elegant APIs and language features

    View full-size slide

  2. About me
    • Product Architect at Dynatrace
    • Focus: Front-End Development, Serverless, JavaScript runtimes
    • Organizing Meetups and Conferences
    • Co-host Working Draft Podcast
    • fettblog.eu

    View full-size slide

  3. Experience, Excitement, Expectations
    What about you?

    View full-size slide

  4. What is idiomatic programming?

    View full-size slide

  5. Buschmann, 1996
    “Idioms are low-level patterns speci ic to a
    programming language”

    View full-size slide

  6. 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 […]”

    View full-size slide

  7. 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

    View full-size slide

  8. 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

    View full-size slide

  9. Idiomatic Rust
    A spectrum
    Syntax, Naming, Plain semantics Traits, libraries, conventions Design patterns

    View full-size slide

  10. 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

    View full-size slide

  11. 1. Readable code
    Tooling, Formatting, Naming, Syntax

    View full-size slide

  12. 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

    View full-size slide

  13. 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

    View full-size slide

  14. 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

    View full-size slide

  15. Clippy
    Results

    View full-size slide

  16. 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

    View full-size slide

  17. 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`

    View full-size slide

  18. 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());
    }
    }

    View full-size slide

  19. 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 {
    Some(42)
    }
    Everything is an expression, until it is
    terminated by a semicolon
    Use this to execute code where you just
    need it

    View full-size slide

  20. 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!"),
    };

    &

    View full-size slide

  21. 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,
    };

    View full-size slide

  22. 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!

    View full-size slide

  23. 2. Traits
    Derivables, Conversions

    View full-size slide

  24. 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

    View full-size slide

  25. 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

    View full-size slide

  26. 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

    View full-size slide

  27. Implement common traits
    Non-Derivable traits
    • Eq: Equality Comparing
    • Ord: Ordering using equivalence / partial equivalence
    • Display: Output via {}

    View full-size slide

  28. 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`" }

    View full-size slide

  29. 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,
    }

    View full-size slide

  30. 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)]

    View full-size slide

  31. 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 }
    }
    }

    View full-size slide

  32. 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() }

    View full-size slide

  33. 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

    View full-size slide

  34. Conversions
    Examples from the standard library
    • From is implemented for u32 because a smaller integer can always
    be converted to a bigger integer.
    • From is not implemented for u16 because the conversion may not
    be possible if the integer is too big.
    • TryFrom is implemented for u16 and returns an error if the integer
    is too big to it in u16.

    View full-size slide

  35. 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 {
    let coords: Vec<&str> = s
    .trim_matches(|p| p == '(' || p == ')')
    .split(',')
    .collect();
    let x_fromstr = coords[0].parse::()?;
    let y_fromstr = coords[1].parse::()?;
    Ok(Point {
    x: x_fromstr,
    y: y_fromstr,
    })
    }
    }

    View full-size slide

  36. 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.

    View full-size slide

  37. 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

    View full-size slide

  38. 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 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

    View full-size slide

  39. A clock
    Make the clock compatible
    • We now can convert any i32 into a Clock.
    • Implementing From gets us Into for free!
    impl From for Clock {
    fn from(val: i32) -> Clock {
    Clock::new(0, val)
    }
    }

    View full-size slide

  40. Conversions
    Into Option
    fn foo(lorem: &str, ipsum: Option, dolor: Option, sit: Option) {
    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(lorem: &str, ipsum: I, dolor: D, sit: S)
    where
    I: Into>,
    D: Into>,
    S: Into>,
    {
    println!("{}", lorem);
    }
    fn main() {
    foo("bar", None, None, None);
    foo("bar", 42, None, None);
    foo("bar", 42, 1337, -1);
    }

    View full-size slide

  41. 3. Structs
    Enums, Structs, Error Handling, Iterators

    View full-size slide

  42. 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

    View full-size slide

  43. 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),
    }

    View full-size slide

  44. 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 for Miles {
    fn from(km: Kilometers) -> Self {
    Miles(km.0 / 1.609)
    }
    }
    impl From for Kilometers {
    fn from(miles: Miles) -> Self {
    Kilometers(miles.0 * 1.609)
    }
    }

    View full-size slide

  45. 4. Error handling

    View full-size slide

  46. 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 for results from operations that can error
    • Option 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)

    View full-size slide

  47. 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 {
    if divisor == 0 {
    None
    } else {
    Some(dividend / divisor)
    }
    }

    View full-size slide

  48. Option
    Utility functions
    • Like with other enums, there are lots of utility functions implemented.
    fn read_file(filename: Option<&str>) -> Result {
    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

    View full-size slide

  49. Result
    • Use Result 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.

    View full-size slide

  50. Result
    Deal with it!
    fn read_username_from_file(path: &str) -> Result {
    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),
    }
    }

    View full-size slide

  51. Result
    Deal with it!
    fn read_username_from_file(path: &str) -> Result {
    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

    View full-size slide

  52. Result
    Deal with it!
    fn read_username_from_file(path: &str) -> Result {
    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

    View full-size slide

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

    View full-size slide

  54. Result
    Ignore it
    fn read_username_from_file(path: &str) -> Result {
    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

    View full-size slide

  55. Result
    Panic!
    • Decide to panic the program when an error occurs
    fn read_username_from_file(path: &str) -> Result {
    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)
    }

    View full-size slide

  56. Result
    Panic!
    • Decide to panic the program when an error occurs
    fn read_username_from_file(path: &str) -> Result {
    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)
    }

    View full-size slide

  57. 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 {
    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)
    }

    View full-size slide

  58. Result
    Propagate the error
    • If your function returns Result, 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 {
    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)
    };
    }

    View full-size slide

  59. 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)
    }
    }

    View full-size slide

  60. 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 {
    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)
    }

    View full-size slide

  61. 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 {
    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

    View full-size slide

  62. 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 {
    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

    View full-size slide

  63. 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::();
    prep.chars().unique().count() == prep.chars().count()
    }

    View full-size slide

  64. 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.

    View full-size slide

  65. 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

    View full-size slide

  66. 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 {
    if self.count < 5 {
    self.count += 1;
    Some(self.count)
    } else {
    None
    }
    }
    }

    View full-size slide

  67. 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

    View full-size slide

  68. 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?

    View full-size slide

  69. 4. Design Patterns
    Extension traits, Builders, Typestate

    View full-size slide

  70. 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.

    View full-size slide

  71. Extension Traits
    pub trait GrandResultExt {
    fn party(self) -> Self;
    }
    impl GrandResultExt for Result> {
    fn party(self) -> Result> {
    if self.is_ok() {
    println!("Wooohoo! '");
    }
    self
    }
    }
    let fortune = Ok("32".to_string())
    .party()
    .unwrap_or("Out of luck.".to_string());

    View full-size slide

  72. 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

    View full-size slide

  73. Builders
    • We start out like we would
    otherwise
    • except that we have empty
    collections and Options
    pub struct Command {
    program: String,
    args: Vec,
    cwd: Option,
    // etc
    }
    impl Command {
    pub fn new(program: String) -> Command {
    Command {
    program: program,
    args: Vec::new(),
    cwd: None,
    }
    }

    }

    View full-size slide

  74. 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
    }
    }

    View full-size slide

  75. 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 {
    /* ... */
    }
    }

    View full-size slide

  76. 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();

    View full-size slide

  77. 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

    View full-size slide

  78. 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?

    View full-size slide

  79. Typestate
    Introduce state as types
    • Structs without ields —> unit
    structs or markers
    struct TwelveHourClock;
    struct TwentyFourHourClock;
    #[derive(Debug, Default)]
    struct Clock {
    hours: i32,
    minutes: i32,
    kind: std::marker::PhantomData,
    }
    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

    View full-size slide

  80. 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()
    }
    }

    View full-size slide

  81. Typestate
    Split implementations
    • We now can have different implementations for our state!
    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)
    }
    }
    impl std::fmt::Display for Clock {
    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)
    }
    }

    View full-size slide

  82. Typestate
    Conversions
    impl Clock {
    fn as_twelve_hour_clock(&self) -> Clock {
    Clock {
    hours: self.hours,
    minutes: self.minutes,
    kind: std::marker::PhantomData,
    }
    }
    }
    impl Clock {
    fn as_twenty_four_hour_clock(&self) -> Clock {
    Clock {
    hours: self.hours,
    minutes: self.minutes,
    kind: std::marker::PhantomData,
    }
    }
    }

    View full-size slide

  83. 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?

    View full-size slide

  84. 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

    View full-size slide