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

Rust_in_a_nutshell__part_2_.pdf

Valeryi Savich
May 14, 2024
10

 Rust_in_a_nutshell__part_2_.pdf

Valeryi Savich

May 14, 2024
Tweet

Transcript

  1. Primitive types Type Rust keyword Boolean bool Char char String

    String / str Integer 8-bit i8 Integer 16-bit i16 Integer 32-bit i32 Unsigned integer 8-bit u8 Unsigned integer 16-bit u16 Unsigned integer 32-bit u32 Float 32-bit f32 Float 64-bit f64 - Rust tries to be minimalistic in declaring any kind of types
  2. Primitive types *More info: Rust data types What about arrays

    with slices? let example = (1, “hello"); // General usage let result: (i32, &str) = (1, "hello"); // With the declared types // Extracting values from the tuple by the index let tuple = (1, 2, 3); let first = tuple.0; let second = tuple.1; let third = tuple.2; and tuples // Arrays let arr = [0, 1, 2, 3, 4]; // Regular declaration let generic_arr = [i32; 3]; // Generic, each element will be initialized to 0 // Slices let middle = &arr[1..4]; // A slice of arr: just the elements 1, 2, and 3 let complete = &arr[..]; // A slice containing all of the elements in arr
  3. Loops & iterators - Rust doesn’t have “C-style” for loop

    on purpose - for loop construction is similar to Python / Ruby - The continue / break instructions also can be used in loops let vector = vec![1,2,3,4,5]; for value in vector { println!("{}", value); // value: i32 } - Use while / loop keywords for implement an in fi nite loop let mut x = 5; let mut done = false; while !done { x += 3; println!("{}", x); if x % 5 == 0 { done = true; } } let mut x = 5; loop { x += 3; println!("{}", x); if x % 5 == 0 { break; } } *More info: for keyword, while and loop keywords
  4. Loops & iterators - As an alternative to loops it

    is possible to use iterator adapters for collections - Built-in collections provide a lot of common adapters (but you also can write your own!) *More info: Iterator trait, Iterators in rust (1..1000) .filter(|&x| x % 2 == 0) .filter(|&x| x % 3 == 0) .take(5) .collect::<Vec<i32>>(); a collection consumer iterator adapters
  5. Functions fn fibonacci(n: u32) -> u32 { if n ==

    0 { return 0 } else if n == 1 { return 1; } fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2) } early returns argument type function name result argument - Looks similar as in other existing programming languages returned result - By default functions are private. Use the pub keyword to make it public - The last expression used as the returned result (implicit return) - Functions without speci fi ed result type returns an empty tuple *More info: Functions
  6. Enums & pattern matching *More info: serde_json::value::Value enum Value {

    Null, Bool(bool), Number(f64), String(String), Array(Box<Vec<Value>>), Object(HashMap<String, Value>), } - Inspired by OCaml - Values can be extracted in a controlled way - Compiler always checks that all cases are covered
  7. Enums & pattern matching *Link to the example use serde_json::{json,

    Value}; fn main() { let example = json!({ "key": "value" }); match example { Value::Null => println!("Null"), Value::Bool(val) => println!("Boolean: {}", val), Value::Number(val) => println!("Number: {}", val), Value::Array(_) | Value::Object(_) => { println!("It's a collection"); }, _ => println!("Other"), }; }
  8. Structures *Link to the example - Similar to structures in

    C - All fi elds are private by default - No data inheritance: use composition for constructing objects struct User { username: String, email: String, sign_in_count: u64, active: bool, } // ... let user = User { username: String::from("User"), email: String::from("[email protected]"), sign_in_count: 5, active: true, };
  9. Structures *Link to the example impl User { pub fn

    new( username: &String, email: &String, sign_in_count: u64, active: bool ) -> Self { User { username: username.to_owned(), email: email.to_owned(), sign_in_count: sign_in_count, active: active, } } } constructor call // ... let user = User::new( &String::from("User"), &String::from("[email protected]"), 5, true, ); string copy - It’s quite common practice to write constructors for structs - Constructor can have any name that developer would like to use - Usage the new(…) method is the rule of thumb
  10. Structures *Link to the example - An alternative way is

    to use the builder pattern - A good choice for scenarios when arguments are too many - Usage the Default::default() method is the rule of thumb struct User { username: Option<String>, email: Option<String>, sign_in_count: u64, active: bool, } impl User { pub fn with_username(mut self, username: &String) -> Self { self.username = Some(username.to_owned()); self } pub fn with_email(mut self, email: &String) -> Self { self.email = Some(email.to_owned()); self } // ... } Wrapped in Option<T>
  11. Structures *Link to the example impl Default for User {

    fn default() -> Self { User { username: None, email: None, sign_in_count: 0, active: true, } } } fn main() { let user = User::default() .with_username(&String::from("User")) .with_email(&String::from("[email protected]")); println!("{:?}", user); } No data by default
  12. Traits - Consider this abstraction as an interface - The

    implementation must satisfy the requirements - May contain methods with default implementation - May extend other types pub trait Installer { fn new() -> Self where Self: Sized; fn get_template_name(&self, path: &String) -> Result<String, Error>; fn install(&self, path: &String, template_name: &String) -> Result<(), Error>; } pub struct GitInstaller; impl Installer for GitInstaller { fn new() -> Self { GitInstaller {} } fn get_template_name(&self, url: &String) -> Result<String, Error> { // ... } fn install(&self, url: &String, template_name: &String) -> Result<(), Error> { // ... } } *More info: Dynamic dispatch; Installer trait, implementation
  13. Traits *More info: Dynamic dispatch; allocating instance, method call fn

    get_installer_from_enum(&self, value: &InstallerTypeEnum) -> Box<dyn Installer> { match value { InstallerTypeEnum::Git => Box::new(GitInstaller::new()), InstallerTypeEnum::Local => Box::new(LocalInstaller::new()), } } fn install_template( &self, installer_type: &InstallerTypeEnum, path: &String, template_name: &Option<String>, ) -> Result<(), Error> { let worker = self.get_installer_from_enum(installer_type); // ... worker.install(path, &used_template_name) } allocation in heap dynamic dispatch
  14. General information - Rust doesn’t have exceptions for errors handling

    - But using Option<T> and Result<T, E> enums instead - Errors are explicitly propagated - In general, consider error handling in Rust as the case analysis to determine whether an operation was successful or not enum Option<T> { Some(T), None, } enum Result<T, E> { Ok(T), Err(E), } - Re fl ects a possibility of data absence - Semantically equivalent to the type Option<T> = Result<T, ()> - Re fl ects a possibility of error
  15. General information - Be quite careful when using .unwrap() because

    it throws panics - Throwing panics means that you have a BUG in code (or in the dependency) and can be caused by: •Usage of the .unwrap() calls in code •Explicit usage of the panic! / unreachable! macro •Unsafe blocks with unhandled errors or invalid pointers Function name Description unwrap() Moves the value v out of the Option<T> if it is Some(v) unwrap_or(val) Returns the contained value or a default unwrap_or_default() Returns the contained value or a default unwrap_or_else(func) Returns the contained value or computes it from a closure. Links: Option<T> and Result<T, E> The Option<T> and Result<T, E> enums are implement a lot various traits so you could choose the best call for the certain case. For example:
  16. A simple example *Link to the example use std::fs::File; use

    std::io::{Read, Write}; use std::path::Path; use tempfile::NamedTempFile; fn file_double(file_path: &Path) -> i32 { let mut file = File::open(file_path).unwrap(); // error 1 let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); // error 2 let n: i32 = contents.trim().parse().unwrap(); // error 3 2 * n } fn main() { let mut file = NamedTempFile::new().unwrap(); let text = "10"; file.write_all(text.as_bytes()).unwrap(); let file_path = file.path(); let doubled = file_double(&file_path); println!("{}", doubled); }
  17. Iteration #1 *Link to the example fn file_double(file_path: &Path) ->

    Result<i32, String> { let mut file = match File::open(file_path) { Ok(file) => file, Err(err) => return Err(err.to_string()), }; let mut contents = String::new(); if let Err(err) = file.read_to_string(&mut contents) { return Err(err.to_string()); } let n: i32 = match contents.trim().parse() { Ok(n) => n, Err(err) => return Err(err.to_string()), }; Ok(2 * n) } fn main() { // … let file_path = file.path(); // Path::new("./file.txt") for IoError match file_double(&file_path) { Ok(value) => println!("Result: {}", value), Err(err) => println!("Error: {}", err), } }
  18. But we can make it much better… - Let’s implement

    a custom Error enum for handling potential errors - Convert the incoming errors into our types or store them in enums - Replace match expressions onto the try! macro - Use crates that eliminates boilerplate code for handling errors, e.g. • anyhow • thiserror • quick-error • error-chain • failure macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(::std::convert::From::from(err)), }); } The try! macro abstracts case analysis like combinators, but unlike combinators, it also abstracts control flow. Namely, it can abstract the early return pattern. - (c) The Rust programming language book
  19. Iteration #2 *Link to the example use std::fs::File; use std::io::{Read,

    Write}; use std::path::Path; use tempfile::NamedTempFile; fn file_double(file_path: &Path) -> Result<i32, String> { let mut file = try!(File::open(file_path).map_err(|e| e.to_string())); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(|e| e.to_string())); let n = try!(contents.trim().parse::<i32>().map_err(|e| e.to_string())); Ok(2 * n) } fn main() { let mut file = NamedTempFile::new().unwrap(); let text = "10"; // "not a value" for ParseError file.write_all(text.as_bytes()).unwrap(); let file_path = file.path(); // Path::new("./file.txt") for IoError match file_double(&file_path) { Ok(value) => println!("Result: {}", value), Err(err) => println!("Error: {}", err), } }
  20. Iteration #3 *Link to the example use std::fs::File; use std::io::{self,

    Read, Write}; use std::path::Path; use std::num; use tempfile::NamedTempFile; // We derive `Debug` because all types should probably derive `Debug`. // This gives us a reasonable human-readable description of `CliError` values. #[derive(Debug)] enum CliError { Io(io::Error), Parse(num::ParseIntError), } fn file_double(file_path: &Path) -> Result<i32, CliError> { let mut file = try!(File::open(file_path).map_err(CliError::Io)); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(CliError::Io)); let n: i32 = try!(contents.trim().parse().map_err(CliError::Parse)); Ok(2 * n) }
  21. Iteration #4 *Link to the example #[derive(Debug)] enum CliError {

    Io(io::Error), Parse(num::ParseIntError), } impl From<io::Error> for CliError { fn from(err: io::Error) -> CliError { CliError::Io(err) } } impl From<num::ParseIntError> for CliError { fn from(err: num::ParseIntError) -> CliError { CliError::Parse(err) } } fn file_double(file_path: &Path) -> Result<i32, CliError> { let mut file = try!(File::open(file_path)); let mut contents = String::new(); try!(file.read_to_string(&mut contents)); let n: i32 = try!(contents.trim().parse()); Ok(2 * n) }
  22. Final iteration *Link to the example use quick_error::quick_error; quick_error! {

    #[derive(Debug)] pub enum CliError { Io(err: io::Error) { from() description("io error") display("I/O error: {}", err) cause(err) } Parse(err: num::ParseIntError) { from() description("parse error") display("Parse error: {}", err) cause(err) } } } fn file_double(file_path: &Path) -> Result<i32, CliError> { let mut file = File::open(file_path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; let n: i32 = contents.trim().parse()?; Ok(2 * n) }
  23. Crates *More about crates Binary Library - Expects to have

    an entrypoint - Usually points to the src/main.rs - The result is an executable compiled for the target platform - Doesn't have an entrypoint - Relies on the src/lib.rs - The compilation result is a module that can be imported and re-used in other projects NOTES - The path to main.rs and lib.rs can be overridden in Cargo.toml - Potentially a project may have main.rs & lib.rs fi les
  24. Modules *More about modules - Represents a container with zero

    or more de fi nitions - The approach is quite similar to the Python module(s) design - Must be included in the parent module Python Rust . ├─ src │ ├── api │ │ ├── social.rs │ │ ├── k8s.rs │ │ └── mod.rs │ ├── core │ │ ├── models.rs │ │ ├── grpc.rs │ │ └── mod.rs │ └─ main.rs
 ├─ Cargo.lock └─ Cargo.toml . ├─ src │ ├── api │ │ ├── social.py │ │ ├── k8s.py │ │ └── __init__.py │ ├── core │ │ ├── models.py │ │ ├── grpc.py │ │ └── __init__.py │ └─ app.py 
 ├─ poetry.lock └─ poetry.toml
  25. API visibility & privacy *More about visibility and privacy -

    Available visibility modi fi ers: - pub - visible to everything - pub(crate) - makes an item visible within the current crate - pub(self) - makes an item visible to the current module - pub(super) - makes an item visible to the parent module - pub(in modulePath) - makes an item visible within the provided path - By default, everything is private, with two exceptions: - Associated items in a pub trait are public by default - Enum variants in a pub enum are also public by default
  26. General information - For the assertions Rust provides two macro:

    •assert_eq!(left, right) •assert_ne!(left, right) - The module with tests must be marked with #[cfg(test)] attribute - All used structs, functions must be imported in the test module - Each test inside of test module can be marked with the following attributes: - #[test] - #[should_panic] - #[ignore] - For running tests use the cargo test command
  27. fn sum(a: u32, b: u32) -> u32 { if b

    == 3 { panic!(); } a + b } fn main() { let result = sum(2, 2); println!("Result: {}", result); } *Link to the example #[cfg(test)] mod tests { use crate::sum; #[test] fn correct_test() { assert_eq!(sum(2, 2), 4); } #[test] fn should_fail() { assert_eq!(sum(2, 3), 4); } #[test] #[should_panic] fn test_func_with_panic() { assert_eq!(sum(2, 3), 4); } } An example
  28. *Link to the RestartStrategy implementation Documentation tests - Contains a

    piece of rust code that shows an actual usage - Allows to keep up documentation up to date - Rustdoc also allows to hide non-relevant code to the example - Tests are running via rustdoc CLI
  29. Editors & IDEs - rust-analyzer (LSP) •Requires rustup toolchain for

    installation •Works with the most popular text editors, e.g.: •VSCode •Vim •Emacs - RustRover IDE •An IDE developed by JetBrains •Can be installed as a plugin with other JetBrains IDEs •Have a good integration with debuggers (gdb, lldb)
  30. Books - The Rust programming language (free) - Rust by

    Example (free) - A Gentle Introduction To Rust (free)
  31. Q&A