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
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
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
(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
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
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`
> 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
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!"), }; ✅ &
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, }; ✅
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
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
"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`" }
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, }
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)]
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 } } }
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() }
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
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.
• 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, }) } }
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
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) } }
• 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
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)
• 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) } }
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
{ 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), } }
{ 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
{ 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
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) }
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) }
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) }
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) }; }
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) } }
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) }
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
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
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() }
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.
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
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
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?
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
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 } }
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> { /* ... */ } }
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();
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
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
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?