Slide 1

Slide 1 text

Idiomatic Rust fettblog.eu - oida.dev - rust-training.eu - rust-linz.at Elegant APIs and language features

Slide 2

Slide 2 text

@ddprrt

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Experience, Excitement, Expectations What about you?

Slide 6

Slide 6 text

What is idiomatic programming?

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

1. Readable code Tooling, Formatting, Naming, Syntax

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Clippy Results

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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`

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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!"), }; ✅ &

Slide 23

Slide 23 text

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, }; ✅

Slide 24

Slide 24 text

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!

Slide 25

Slide 25 text

2. Traits Derivables, Conversions

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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.

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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.

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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); } ✅

Slide 43

Slide 43 text

3. Structs Enums, Structs, Error Handling, Iterators

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

4. Error handling

Slide 48

Slide 48 text

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)

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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.

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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.

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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?

Slide 71

Slide 71 text

4. Design Patterns Extension traits, Builders, Typestate

Slide 72

Slide 72 text

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.

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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, } } … }

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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?

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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?

Slide 86

Slide 86 text

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