O slideshow foi denunciado.
Utilizamos seu perfil e dados de atividades no LinkedIn para personalizar e exibir anúncios mais relevantes. Altere suas preferências de anúncios quando desejar.

The Rust Programming Language: an Overview

81 visualizações

Publicada em

Brief overview of the Rust system programming language. Provides a concise introduction of its basic features, with an emphasis on its memory safety features (ownership, moves, borrowing) and programming style with generic functions, structures, and traits.

Publicada em: Engenharia
  • DOWNLOAD FULL BOOKS, INTO AVAILABLE FORMAT ......................................................................................................................... ......................................................................................................................... 1.DOWNLOAD FULL. PDF EBOOK here { https://tinyurl.com/y6a5rkg5 } ......................................................................................................................... 1.DOWNLOAD FULL. EPUB Ebook here { https://tinyurl.com/y6a5rkg5 } ......................................................................................................................... 1.DOWNLOAD FULL. doc Ebook here { https://tinyurl.com/y6a5rkg5 } ......................................................................................................................... 1.DOWNLOAD FULL. PDF EBOOK here { https://tinyurl.com/y6a5rkg5 } ......................................................................................................................... 1.DOWNLOAD FULL. EPUB Ebook here { https://tinyurl.com/y6a5rkg5 } ......................................................................................................................... 1.DOWNLOAD FULL. doc Ebook here { https://tinyurl.com/y6a5rkg5 } ......................................................................................................................... ......................................................................................................................... ......................................................................................................................... .............. Browse by Genre Available eBooks ......................................................................................................................... Art, Biography, Business, Chick Lit, Children's, Christian, Classics, Comics, Contemporary, Cookbooks, Crime, Ebooks, Fantasy, Fiction, Graphic Novels, Historical Fiction, History, Horror, Humor And Comedy, Manga, Memoir, Music, Mystery, Non Fiction, Paranormal, Philosophy, Poetry, Psychology, Religion, Romance, Science, Science Fiction, Self Help, Suspense, Spirituality, Sports, Thriller, Travel, Young Adult,
       Responder 
    Tem certeza que deseja  Sim  Não
    Insira sua mensagem aqui
  • Seja a primeira pessoa a gostar disto

The Rust Programming Language: an Overview

  1. 1. Rust An Overview Roberto Casadei PSLab Department of Computer Science and Engineering (DISI) Alma Mater Studiorum – Università of Bologna https://github.com/metaphori/learning-rust May 26, 2019 R. Casadei Intro Ownership, borrows, moves UDTs More 1/61
  2. 2. Outline 1 Intro 2 Ownership, borrows, moves 3 UDTs 4 More R. Casadei Intro Ownership, borrows, moves UDTs More 2/61
  3. 3. Rust: a one-slide overview Rust is a PL for system programming developed by Mozilla&co System programming is resource-constrained programming—cf., OS, device drivers, FSs, DBs, Media, Memory, Networking, Virtualisation, Scientific simulations, Games.. Rust is a ahead-of-time compiled PL Rust is a type-safe PL (checks ensure well-definedness, i.e., no undefined behaviour) Goal: secure code (memory safety) compile-time checks of ownership, moves, borrows Goal: trustworthy concurrency compile-time checks preventing data races, misuse of sync primitives Who uses Rust? e.g., Dropbox (see https://www.rust-lang.org/production) What developers say? Rust is the most loved PL as StackOverflow surveys 2016-19 Key technical aspects: Rust tracks the ownership and lifetimes of values, so mistakes like dangling pointers, double frees, and pointer invalidation are ruled out at compile time. R. Casadei Intro Ownership, borrows, moves UDTs More 3/61
  4. 4. Toolchain Install: (https://rustup.rs) rustup: the Rust installer / toolchain manager rustc: the Rust compiler Source: main.rs fn main(){ println!("Hello world! {:?}", std::env::args()); // Debug format } Compiling and running $ rustc main.rs $ ./main # Hello world cargo: Rust’s compilation manager, package manager, and general-purpose tool cargo new (--bin|--lib) hello creates a new Rust package under hello/ File Cargo.toml holds metadata for the package cargo build and cargo run Cargo.lock keeps track of the exact versions of deps. Output in target/debug rustdoc: Rust documentation tool On REPL: an open issue (rusti crate exists but has some problems) R. Casadei Intro Ownership, borrows, moves UDTs More 4/61
  5. 5. Project layout(s) Cargo.lock Cargo.toml benches/ large-input.rs examples/ simple.rs src/ bin/ another_executable.rs lib.rs main.rs tests/ some-integration-tests.rs Cargo.toml and Cargo.lock are stored in the root of your package (package root). Source code goes in src/ The default library file is src/lib.rs The default executable file is src/main.rs Other executables can be placed in src/bin/*.rs. Integration tests go in tests/ Unit tests go in each file they’re testing Examples go in examples/ Benchmarks go in benches/ R. Casadei Intro Ownership, borrows, moves UDTs More 5/61
  6. 6. Cargo.toml example cargo.toml example cargo-features = ["default-run"] [package] name = "myproj" version = "0.1.0" authors = ["Roberto Casadei <roberto.casadei90@gmail.com>"] default-run = "myBin1" edition = "2018" # allows omitting "extern crate" decls [[bin]] name = "myBin1" path = "src/main1.rs" [[bin]] name = "myBin2" path = "src/main2.rs" [dependencies] myDep1 = "0.3.2" # on creates.io myDep1b = ">=1.0.5 <1.1.9" myDep2 = { path = "../path/to/my/dep2" } # on filesystem myDep3 = { git = "https://github.com/.../dep3.git", rev = "528f19c" } [profile.debug] # for 'cargo build' [profile.relase] # for 'cargo build --release' debug = true # enable debug symbols in release builds [profile.test] # for 'cargo test' R. Casadei Intro Ownership, borrows, moves UDTs More 6/61
  7. 7. Projects, Packages, Crates, Modules and Items Package: a Cargo project from which one or more crates are built, tested, shared Crate: a Rust package (library or executable)—for sharing code between projects Modules (keyword mod) are namespaces that help organising code within a project Standard library std is automatically linked (but not used) with every project. Operator :: to access module features; self and super Module in files: when Rust sees mod xxx;, it checks for both xxx.rs and xxx/mod.rs On visibility: private by default (siblings + children); pub (through parent) A module is a collection of named features aka items Functions fn f(){ return 0; } Types: user-def types introduced via struct, enum, trait. Type aliases: type Foo = .. Impl blocks: impl MyStruct { ... }, impl SomeTrait for T { ... } Constants: pub const c = 100; pub static s = 200; Module: a module can contain sub-modules (public or private like any other named item) Imports: use and extern crate decls; these are aliases which can be made public extern crate is not needed with edition="2018" in Cargo.toml Extern blocks: declare a set of functions, written in some PL, callable from Rust code Any item can be decorated with attributes: #(test) fn f(){..} R. Casadei Intro Ownership, borrows, moves UDTs More 7/61
  8. 8. Basics (1/5) Comments // Comment /* Comment */ /// Documentation comment let declarations, (immutable) variables, mutables, constants fn main(){ let (v1,v2) = (7.8, true); // IMMUTABILITY by default; also note INFERENCE let v1 = v1+1.; // Shadowing // v1 = v1 + 1; // Error let v: Vec<i32> = Vec::new(); println!("{}", v.len()); // v.push(1); // Error: cannot borrow as mutable let mut v: Vec<u64> = Vec::new(); // Mutables need explicit modifier Blocks: any block surrounded by curly braces can function as an expression let i: i32 = { 10; }; // ERROR: expected i32, found () let i: i32 = { fn f(){}; // block-scoped items can be def 20; ; 10 }; // OK (20 is dropped; empty stmt; return 10) Scalar types let i: i64 = -20000; let j: u8 = 255; let x: f32 = std::f32::INFINITY; let c: char = 'c'; let b: bool = true; let u: () = { println!("hello") }; // unit type let v = (1 as i32)+(2 as i64); // ERROR: can't sum nums of diff type R. Casadei Intro Ownership, borrows, moves UDTs More 8/61
  9. 9. Basics (2/5) Match expressions let r: Result<i32,&str> = Ok(10); let v = match res { Ok(success) => /*..*/, Err(error) => /*..*/ }; Basic I/O println!("{:.4} {} {} {:b} {:?}", std::f64::consts::PI, true, "foo", 7, Some(10)); let mut s: String = String::new(); if let Ok(l) = std::io::stdin().read_line(&mut s) { let i = s[..l-1].parse::<i32>().expect(&format!("Invalid input {}", s)); // ... } Type aliases/constructors // The std::io::Result type, equivalent to the usual `Result`, but // specialized to use std::io::Error as the error type. type Result<T> = std::result::Result<T, Error> Control structures (are expressions) let mut x: f64 = 0.9; let res = if x>0. { /*...*/ } else if x<0. { /*...*/ } else { /*...*/ }; let i: i32 = loop { x = x*x; if x<0.5 { break 10; } }; while x>0.1 { x=x*x; }; // return type must be unit 'bar: while false { break 'bar; } // every loop may use labels for i in (0..10).rev() { /*...*/ }; // loop through a reversed range for e in [1,2,3].iter() { print!("{};",e) }; R. Casadei Intro Ownership, borrows, moves UDTs More 9/61
  10. 10. Basics (3/5) Compound types //// TUPLES let (v1,v2) = (77, true); // Destructuring let tp: (i32,bool) = (77, true); println!("{}", tp.0 as f64); //// ARRAY [T;N]: represents an array of N values of type T let a1: [i32;5] = [1, 2, 3, 4, 5]; // Type include size a1[500]; // compile-time error let last = a1[a1.len()-1]; //// VECTOR Vec<T>: a resizable, heap-alloc'ed buffer of T elements let mut v1: Vec<i32> = Vec::new(); v1.push(10); let v2 = vec![1.0, 2.0, 3.0]; //// STRINGS let s0: &'static str = "foo"; // Literals => static refs to string slice let s1: String = "foo".to_string(); let s2 = String::from("bar"); //// SLICE &[T]: reference to a contiguous region of a homo-collection let aslice: &[i32] = &a1[1..]; let s3: &str = &s2[..s2.len()-1]; //// BOX: a owning pointer to a value in the heap let v: (i32,&str) = (12, "eggs"); let b = Box::new(v); // allocate the tuple on the heap println!("{}", b.1); // Deref coercion A Vec<T> has 3 values: (1) pointer to content on heap; (2) capacity; (3) actual len A reference to a slice &[T] is a fat pointer: a 2-word val comprising (1) ptr to the slice’s first elem, and (2) the num of elems in the slice Box::new(v) allocates some heap space, moves v into it, and returns a Box ptr R. Casadei Intro Ownership, borrows, moves UDTs More 10/61
  11. 11. Basics (4/5) UDTs struct S { x: f32, y: f32 } // Named-field struct struct T(i32,char) // Tuple-like struct struct U {} // Unit-like struct enum E { A, B(u32) } // Enum (sum type) let s1 = S { x: 1.2, y: 5. }; let t1 = T(77,'a'); let u: U = U{}; let e = E::B(2) + E::B(10).inc(); impl E { fn inc(&self) -> Self { match self { E::B(i) => E::B(i+1), _ => E::A } } } pub trait Add<RHS=Self> { // (already defined in stdlib) type Output; fn add(self, rhs: RHS) -> Self::Output; } impl Add for E { type Output = E; fn add(self, rhs: E) -> Self::Output { match (self,rhs) { (B(i),B(j)) => B(i+j), _ => E::A } } } R. Casadei Intro Ownership, borrows, moves UDTs More 11/61
  12. 12. Basics (5/5) Functions and closures fn simple_fun(b: bool) -> i32 { if b { return 1 }; 0 } fn parse_pair<T: FromStr>(s: &str, sep: char) -> Option<(T, T)> { ... } let mut z = 0; let mut f = |x,y| { z+=1; x+y }; // closure (must be mut to change z) Error handling let r: Result<i32,&str> = /* ... */; let v = match res { Ok(success) => /*..*/, Err(error) => /*..*/ }; type RI = Result<i32,String>; fn ferr(r1: RI, r2: RI) -> RI { return Ok(r1?+r2?); } let ri: RI = ferr(Ok(60),Ok(30)); println!("RI: {:?}", ri.unwrap()); // extract success result or panics let v: Option<i32> = ri.ok(); // ERROR: value used here after move println!("Panic recovery: {:?}", std::panic::catch_unwind(|| { let y = 0; let x = 1 / y; x }).ok()); // Panic recovery: None Unit testing: Rust/Cargo provide basic support for testing #[cfg(test)] mod my_tests { #[test] fn test_something() { assert_eq!(10+5, 15); }; R. Casadei Intro Ownership, borrows, moves UDTs More 12/61
  13. 13. Example: program use std::str::FromStr; // brings trait into scope use std::io::Write; fn main() { // doesn't return a value so we omit return type -> let mut numbers: Vec<u64> = Vec::new(); // initialises a local mutable var for arg in std::env::args().skip(1) { if &arg == "help" { writeln!(std::io::stderr(), "Usage: ...").unwrap(); std::process::exit(1); } numbers.push(u64::from_str(&arg) // type u64 impls trait FromStr .expect("error parsing arg")); } println!("{}", numbers[0]); // ending ! denotes macro calls for m in &numbers[1..] { println!("{}", *m); } } Vec is Rust’s growable vector type std::env::args() returns an iterator unwrap() call is a terse way to check the attempt to print the error did not itself fail. from_str returns a Result value which is either Ok(v) or Err(e); method expect extracts the value if ok or panics otherwise. In the iteration, ownership of the vector remains to numbers, and we borrow a reference to the vector’s elements in m via operator &. Operator * is for dereferencing. Rust assumes that if main returns at all, the program finished successfully. R. Casadei Intro Ownership, borrows, moves UDTs More 13/61
  14. 14. Example: guess a number Cargo.toml [dependencies] rand = "0.3.14" extern crate rand; use std::io; use std::cmp::Ordering; use rand::Rng; fn main(){ let secnum = rand::thread_rng().gen_range(1,101); loop{ let mut guess = String::new(); println!("Guess: "); io::stdin().read_line(&mut guess).expect("Failed to read line"); let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; match guess.cmp(&secnum) { Ordering::Less => println!("Too small"), // notice comma Ordering::Greater => println!("Too big"), Ordering::Equal => { println!("You win"); break } } } } Without the use, we had to write std::io::stdin() rand::thread_rng() gives a random gen local to current thread and seeded by OS stdin returns a Stdin instance; read_line() puts text from stdin to the variable passed by reference and returns an object of enum type io::Result admitting values Ok or Err. On a Err value, expect(m) will crash the program displaying m. R. Casadei Intro Ownership, borrows, moves UDTs More 14/61
  15. 15. Example: webserver [dependencies] iron = "0.5.1" mime = "0.2.3" router = "0.5.1" urlencoded = "0.5.0" extern crate iron; extern crate router; #[macro_use] extern crate mime; use iron::prelude::*; use iron::status; use router::Router; fn main() { let mut router = Router::new(); router.get("/", get_logic, "root"); router.get("/hello", get_logic, "pageId1"); println!("Serving on http://localhost:3000..."); Iron::new(router).http("localhost:3000").unwrap(); } fn get_logic(_request: &mut Request) -> IronResult<Response> { let mut response = Response::new(); response.set_mut(status::Ok); response.set_mut(mime!(Text/Html; Charset=Utf8)); response.set_mut(r#"<h1>Hello</h1>"#); Ok(response) } R. Casadei Intro Ownership, borrows, moves UDTs More 15/61
  16. 16. Outline 1 Intro 2 Ownership, borrows, moves 3 UDTs 4 More R. Casadei Intro Ownership, borrows, moves UDTs More 16/61
  17. 17. On memory management Stack: LIFO; fast to write; fast to read since it must take up a known, fixed size. Heap: to store data with unknown size at compile time or dynamic size; you allocate some space and get a pointer to the address of that location. Pattern: fixed-size handle on stack pointing to a variable amount of data on the heap struct Person { name: String, birth: i32 } let mut composers = Vec::new(); composers.push(Person {name: 'Palestrina'.to_string(), birth: 1525 }); // ... Var composers own its vector; the vector owns its elems; each elem is a Person structure which holds its fields; and the string field owns its text. R. Casadei Intro Ownership, borrows, moves UDTs More 17/61
  18. 18. Rust memory management and safety Rust makes the following pair of promises fundamental to system-programming 1) You decide the lifetime of each value of your program. Rust frees memory/resources belonging to a value promptly, at a point under your control. 2) Your program will never use a dangling pointer (i.e., ptr to an object which has been freed). Note: C/C++ provide (1) but not (2) How does Rust give control to programmers over values’ lifetimes while keeping the language safe? It does so by restricting how your programs can use pointers. These constraints allow Rust’s compile-time checks to verify that your program is free of memory safety errors: dangling pointers, double frees, using uninitialized memory, etc Same rules also form the basis of Rust’s support for safe concurrent programming Note: Rust does provide C-like, raw pointers but these can only be used in unsafe code (defined by unsafe{} blocks) R. Casadei Intro Ownership, borrows, moves UDTs More 18/61
  19. 19. Ownership: intro Some PLs use GCs Other PLs require explicit de/allocation of memory. Rust uses a third approach: memory is managed through a system of ownership with a set of rules that the compiler checks at compile time Ownership—the idea: the owning object decides when to free the owned object Ownership rules: 1) Each value in Rust has a unique owner 2) When the owner is dropped, the owner value is dropped too. A variable owns its value. A variable is dropped when it goes out of scope, and its value is dropped along with it. This is similar to Resource Acquisition Is Initialization (RAII) idiom in C++. Owners and owned values form trees Rust provides flexibility to the ownership model via Move of values from a owner to another (hence rearranging the “ownership tree”) ○ Ability to “borrow a reference” to a value, through a nonowning pointer with limited lifetime ○ Ref-counted pointer types Rc/Arc that allow many owners, under some restrictions ○ R. Casadei Intro Ownership, borrows, moves UDTs More 19/61
  20. 20. Ownership: Move (1/2) In Rust, by default (i.e., if a type doesn’t impl the Copy trait), operations like assigning a value to a variable, passing it to a function, or returning it from a function don’t copy the value: they move it. In Rust, a move leaves its source uninitialized, as the destination takes ownership let s = vec!["udon".to_string(),"ramen".to_string(),"soba".to_string()]; let t = s; // let u = s; // Rejected as, after move, s is unitialised!!! More ops that move If you assign to a var which was already initialised, Rust drops the var’s prior value. Passing arguments to functions moves ownership to the function’s parameters. Returning a value from a function moves ownership to the caller. Building a tuple moves the values into the tuple; etc.... R. Casadei Intro Ownership, borrows, moves UDTs More 20/61
  21. 21. Ownership: Move (2/2) Move examples let mut s = "bob".to_string(); s = "mark".to_string(); // value "bob" dropped here let t = s; // ownership moves from s to t s = "ste".to_string(); // nothing dropped here (s was uninitialised) fn gen_vec() -> Vec<i32> { let v = vec![1,2,3]; return v; } let v = gen_vec(); Moves and control flow let x = vec![10,20,30]; let y = x.clone(); if ... { f(x) } else { g(x) } h(x); // bad: x is unitialised here if either path uses it while ... { g(x); ... } // bad: x moved in first iteration, unitialised in 2nd Moves and indexed content let mut v = vec!["bob".to_string(), "mark".to_string()]; let x = v[0]; // ERROR: cannot move out of indexed content (note: ok with int vals) // Other operations do support moving elements out let y = v.pop().unwrap(); // pop value off the end of the vector (ok) for mut s in v { // for loop takes ownership of the vector s.push('!'); } println!("{:?}",v); // ERROR: value 'v' used after move R. Casadei Intro Ownership, borrows, moves UDTs More 21/61
  22. 22. Ownership: on copy Assigning a value of a Copy type copies the value, rather than moving it. The source of the assignment (or argument passing) remains initialized and usable. As a general rule, simple scalar values can be Copy, and nothing that requires allocation or is some form of resource is Copy—e.g.,: all integer types; bools; chars; floating-point types; tuples or fixed-size arrays of only Copy types. On custom types: By default, struct and enum types are not Copy. #[derive(Copy, Clone)] struct Label { number: u32 } // types of fields must impl Copy as well Copy examples fn fs(s: String) { println!("fs: {}", s); } fn fi(i: i32){ println!("fi: {}", i); } fn give_s() -> String { let s = String::from("!"); s } // Ownership moved, nothing dealloc'ed fn chg_s(mut s: String) -> String { s.push_str("!"); return s; } // Borrows mutably let mut s = "hello".to_string(); fs(s); // moves ownership of 's' to the function! // println!("s: {}", s); // Cannot reference 's' let i = 10; fi(i); // i gets copied println!("i: {}", i); // i can be used here let s2 = give_s(); // give_s() moves its return value to s2 s2 = chg_s(s2) // takes ownership and returns it R. Casadei Intro Ownership, borrows, moves UDTs More 22/61
  23. 23. Ownership: Rc and Arc—shared ownership Rust provides safe reference-counted pointer types: Rc and Arc Rc uses faster non-thread-safe code to update its ref count. use std::rc::Rc; let s: Rc<String> = Rc::new("bob".to_string()); let t: Rc<String> = s.clone(); // Does not copy content; creates a new pointer let u: Rc<String> = s.clone(); // You can use T's methods directly on Rc<T> assert!(s.contains("ob")) A value owned by an Rc pointer is immutable * Rust’s memory and thread-safety guarantees depend on ensuring that no value is ever simultaneously shared and mutable. Arc is safe to share between threads directly (“atomic reference count”) A well-known problem with using reference counts is cycles (causing memory leaks). R. Casadei Intro Ownership, borrows, moves UDTs More 23/61
  24. 24. Beyond ownership: references and borrowing (1/3) References are nonowning pointer types allowing you to refer to a value without taking ownership of it; i.e., they have no effect on their referents’ lifetimes. Rust refers to creating a reference to some value as borrowing the value: what you have borrowed, you must eventually return to its owner fn calc_len(r: &String) -> usize { s.len() } fn chg_s2(s: &mut String) { s.push_str("!") } let mut s = String::from("bob"); let len = calc_len(&s); chg_s2(&mut s); println!("String: {} ; Length: {}", s, len); r is a ptr in stack, pointing to s handle in stack (ptr to content in heap + len + capacity) So, passing args by-value moves ownership (of val to fun), by-ref make fun borrowing the val. Two types of references: 1) Shared ref: typed &T, lets you read but not modify its referent &v; “srefs” are Copy 2) Mutable ref: typed &mut T, lets you read or modify its referent &v; “mrefs” are exclusive and AREN’T Copy − Kinda way to enforce “multiple readers, single writer” rule at compile time (also applies to owners: as long as there are shared refs to a val, not even its owner can modify it) This restriction allows you to avoid data race at compile time. R. Casadei Intro Ownership, borrows, moves UDTs More 24/61
  25. 25. Beyond ownership: references and borrowing (2/3) On dangling refs: in Rust the compiler guarantees that refs will never be dangling, by ensuring the data will not go out of scope before the ref to the data does. fn dangle()->&String{let s = String::from(""); &s} // Missing lifetime specifier fn main() { let reference_to_nothing = dangle(); } Rules of references: (1) You can have either but not both: one mutable ref or any num of immutable refs (2) References must always be valid let mut y = 32; let m = &mut y; // &mut y is a mutable reference to y *m += 32; // explicitly dereference m to set y's value struct Boy { name: &'static str, age: i32 }; let p = Boy { name: "Bob", age: 10 }; let r = &p; assert_eq!(r.name, "Bob"); // Implicit dereference assert_eq!((*r).name, "Bob"); // Explicit dereference The . op can implicitly borrow a ref to its left operand, if needed for a method call. Refs of refs: ops like . and == can follows as many refs as needed to find their targets. let mut v = vec![1973, 1968]; v.sort(); // implicitly borrows a mutable reference to v (&mut v).sort(); // equivalent; much uglier Assigning references: assigning a ref makes it point to a new value let x = 10; let y = 20; let mut r = &x; r = &y; assert!(*r == 20); In C++, assigning a ref stores the val in its referent; and you can’t change the pointer of a ref. R. Casadei Intro Ownership, borrows, moves UDTs More 25/61
  26. 26. Beyond ownership: references and borrowing (3/3) No null references: Rust references are never null. There’s no analogue to C’s NULL or C++’s nullptr; there is no default initial value for a reference (you can’t use any var until it’s been initialized, regardless of its type); and Rust won’t convert integers to references (outside of unsafe code). In Rust, if you need a value that is either a reference to something or not, use the type Option<&T>. At the machine level, None is repr as a null pointer, and Some(r), where r is a &T value, as the nonzero address, so Option<&T> is just as efficient as a nullable pointer in C or C++, but safer. Borrowing refs to arbitrary expressions fn factorial(n: usize) -> usize { (1..n+1).fold(1, |a, b| a * b) } let r = &factorial(6); // &720 assert_eq!(r + &1009, 1729); For this situations, Rust creates an anonymous var to hold the expr’s value and makes a ref point to that; that var’s lifetime depends on what you do with the ref. Refs to slices and trait objects A ref to a slice is a fat pointer, carrying the starting address of the slice and its length. A ref to a trait (trait object) is also a fat pointer: carries a value’s address and a pointer to the trait’s implementation appropriate to that value. R. Casadei Intro Ownership, borrows, moves UDTs More 26/61
  27. 27. Ref safety and lifetimes » borrowing a local var You can’t borrow a ref to a local var and take it out of the var’s scope. { let r; { let x = 1; r = &x; }; assert_eq!(*r,1); // STATIC ERROR (dangling ref) } The Rust compiler tries to assign each ref in your program a lifetime If you have a var x, then a ref to x must not outlive x itself. If you store a reference in a var r, the reference must be good for the entire lifetime of the var. Ǧ If you borrow a ref r to a part of a data struct d, then d’s lifetime must enclose r’s. let d = vec![10,20,30]; let r = &d[1]; Ǧ If you store a ref r in some data struct d, then r’s lifetime must enclose d’s lifetime. Other features introduce other constraints, but the principle is the same: (1) understand constraints arising from how the program uses refs; (2) find lifetimes to satisfy them. R. Casadei Intro Ownership, borrows, moves UDTs More 27/61
  28. 28. Ref safety and lifetimes » refs as params Consider a function that takes a ref and stores it in a global var. Rust’s equivalent of a global var is called a static: a value created when the program starts and dropped when it terminates. Mutable statics are inherently not thread-safe, so you may access them only within an unsafe block. static mut STASH: &i32 = &0; // statics must be initialised fn f(p: &i32) { // Actually a shortcut for: fn f<'a>(p: &'a i32) unsafe { STASH = p; } // COMPILE-TIME ERROR: lifetime `'static` required } You may read <'a> as “for any lifetime 'a” Explanation: Since STASH lives for the program’s entire execution, the reference type it holds must have a lifetime of the same length; Rust calls this the 'static lifetime. But the lifetime of p’s reference is some 'a, which could be anything, as long as it encloses the call to f. So, Rust rejects our code. I.e., the only way to ensure we can’t leave STASH dangling, is to apply f only to references to other statics. Notice how we would need to update the function’s signature to make it evident its behaviour wrt the borrowing parameter. R. Casadei Intro Ownership, borrows, moves UDTs More 28/61
  29. 29. Ref safety and lifetimes » passing refs as args fn g<'a>(p: &'a i32){ ... } let x = 10; g(&x); // OK: ref &x does not outlive x and encloses the entire call to g fn h(p: &'static i32) { ... } h(&x); // STATIC ERROR: ref &x must not outlive x but we constrain it to be static From g’s signature alone, Rust knows it will not save p anywhere that might outlive the call; since any lifetime that encloses the call must work for 'a, Rust chooses the smallest possible lifetime for &x: that of the call to g R. Casadei Intro Ownership, borrows, moves UDTs More 29/61
  30. 30. Ref safety and lifetimes » returning refs It’s common for a function to take a reference to some data structure, and then return a reference into some part of that structure. fn smallest(v: &[i32]) -> &i32 { // fn smallest<'a>(v: &'a [i32]) -> &'a i32 {...} let mut s = &v[0]; for r in &v[1..] { if *r < *s { s = r; } } s } let s; { let parabola = [9, 4, 1, 0, 1, 4, 9]; s = smallest(&parabola); } assert_eq!(*s, 0); // bad: points to element of dropped array When returning a reference from a function, the lifetime parameter for the return type must match the lifetime parameter for one of the parameters (otherwise, it’d refer to a value created within this function, which would be a dangling ref, or static data!). Argument &parabola must not outlive parabola itself; yet smallest’s return value must live at least as long as s. There’s no possible lifetime 'a that can satisfy both constraints, so Rust rejects the code. R. Casadei Intro Ownership, borrows, moves UDTs More 30/61
  31. 31. Ref safety and lifetimes » structs containing refs In structs, a field of ref type (or type parametric on lifetime) must also specify the lifetime struct S1<'a> { r: &i32 } // ERROR: expected lifetime parameter for 'r: &i32' struct S2<'a> { // S type has a lifetime (just like reference types do) r: &'a i32 // Lifetime of any ref you store in r must enclose 'a // and 'a must outlast the lifetime of wherever you store the S } // Each S value get a fresh lifetime 'a constrained by how you use the value struct T1 { s: S2 } // ERROR: expected lifetime parameter for 's: S' struct T2 { s: S2<'static> } // may only borrow values that live the entire program struct T3<'a> { s: S2<'a> } // relate T value's lifetime to that of the ref its S holds let s; { let x = 10; s = S2 { r: &x }; // ERROR: borrowed value does not live long enough } assert_eq!(*s.r, 10); // otherwise, this would be bad On lifetimes It’s not just references and struct types that have lifetimes. Every type in Rust has a lifetime, including i32 and String. Most are simply ’static , meaning that values of those types can live for as long as you like; e.g., a Vec<i32> is self-contained, and needn’t be dropped before any particular variable goes out of scope. But a type like Vec<&'a i32> has a lifetime that must be enclosed by 'a: it must be dropped while its referents are still alive R. Casadei Intro Ownership, borrows, moves UDTs More 31/61
  32. 32. Ref safety and lifetimes » Distinct lifetime parameters struct S<'a> { x: &'a i32, y: &'a i32 } let x = 10; let r; { let y = 20; { let s = S { x: &x, y: &y }; // s' lifetime must not outlive x's and y's r = s.x; // s.x and s.y have same lifetime and s.y cannot outlive y } } // ERROR: y does not live long enough // SOLUTION struct S<'a, 'b> { x: &'a i32, y: &'b i32 } With the last def, s.x and s.y have independent lifetimes. What we do with s.x has no effect on what we store in s.y, so it’s easy to satisfy the constraints now: 'a can simply be r’s lifetime, and 'b can be s’s. (y’s lifetime would work too for 'b, but Rust tries to choose the smallest lifetime that works.) R. Casadei Intro Ownership, borrows, moves UDTs More 32/61
  33. 33. Sharing vs. mutation (1/2) Other situations where Rust protect us against dangling pointers let v = vec![4, 8, 19, 27, 34, 10]; // handle on stack, content on heap let r = &v; let aside = v; // ERROR: cannot move out of `v` because it is borrowed r[0]; // if 'v' had moved to 'aside', here it would be uninitialized Across its lifetime, a shared ref makes its referent read-only. fn extend(vec: &mut Vec<f64>, slice: &[f64]){ for e in slice { vec.push(*e); }} let mut v1 = Vec::new(); let v2 = vec![0.0, 1.0, 0.0, -1.0]; extend(&mut v1, &v2); extend(&mut v1, &v1); // ERR: can't borrow v1 as immutable as also borrowed as mut Here, Rust protects us against a slice turning into a dangling pointer by a vector reallocation R. Casadei Intro Ownership, borrows, moves UDTs More 33/61
  34. 34. Sharing vs. mutation (2/2) Rust’s rules for mutation and sharing: 1) Shared access is read-only access: values borrowed by shared refs are RO 2) Mutable access is exclusive access: a value borrowed by a mutable ref is reachable exclusively via that reference Each kind of reference affects what we can do with (i) the values along the owning path to the referent, and (ii) the values reachable from the referent. let mut v = (7,8); let m = &mut v; v = (8,9); // ERROR: cannot assign to `v` because it is borrowed let r = &v; // ERROR: cannot borrow `v` as imm. cause it is also borrowed as mut let m0 = &mut m.0; // OK reborrowing mutable from mutable *m = (8,9); // ERROR: cannot assign to '*m' because it's borrowed println!("{}",v.1); // ERROR: cannot borrow `v.1` as immutable... R. Casadei Intro Ownership, borrows, moves UDTs More 34/61
  35. 35. Rust makes it difficult to build a “sea of objects” Since the rise of automatic memory management, the default architecture of all programs has been the sea of objects, with many objects related by intricate dependencies Rust, with its ownership model, fosters the constructions of trees of objects I.e., Rust makes it hard to build cycles (two values such that each one contains a reference to the other) You need to use smart pointers like Rc and interior mutability You’ll have a hard time in recreating all OOP antipatterns: Rust’s ownership model will give you some trouble; the cure is to do some up-front design and build a better program Rust is all about transferring the pain of understanding your program from the future to the present. − It works unreasonably well: not only can Rust force you to understand why your program is thread-safe, it can even require some amount of high-level architectural design R. Casadei Intro Ownership, borrows, moves UDTs More 35/61
  36. 36. Outline 1 Intro 2 Ownership, borrows, moves 3 UDTs 4 More R. Casadei Intro Ownership, borrows, moves UDTs More 36/61
  37. 37. Structs Three kinds of struct types: (1) named-field, (2) tuple-like, (3) unit-like Named-field Structs pub struct MyStruct { pub field1: String, pub field2: u64, pub field3: bool, } let field3 = true; let s1 = MyStruct { field1: String::from("abc"), field2: 88, field3 }; // FIELD INIT SHORTHAND let s2 = MyStruct { field3: false, ..st1}; // UPDATE SYNTAX // ISSUE: can't use s1 here On ownership of struct data: notice MyStruct uses owned String type rather than &str string slice type: indeed, we want struct instances to own all of its data . Tuple structs have no names for fields struct Point (i32, i32, i32); // Note: no curly but round brackets let origin = Point(0,0,0); println!("x={}; y={}", origin.0, origin.1); Good for newtypes: structs with a single element that you def to get stricter type checking Convention: CamelCase for types; snake_case for fields and methods Adding useful functionality with derived traits #[derive(Debug)] struct MyStruct { field1: String, field2: u64, field3: bool,} fn print_my_struct(s: &MyStruct){ println!("{:?}",s) }; − Common traits include: Copy, Clone, Debug, PartialEq, PartialCmp R. Casadei Intro Ownership, borrows, moves UDTs More 37/61
  38. 38. Methods and associated functions Methods are fns defined in the context of a struct/enum/trait, which take the receiver as first parameter with special name self (you can omit the type) Static methods are methods that don’t take self as parameter Terminology: Associated items are items (e.g., functions) associated with a type. Each struct can have multiple impl blocks for defining methods/associated functions. #[derive(Debug)] struct Rectangle { width: f64, height: f64 } impl Rectangle { fn to_tuple(self) -> (f64,f64) { (self.width, self.height) } fn area(&self) -> f64 { self.width * self.height } // Borrow self fn enlarge(&mut self, perc: f64) { ... } // Borrow self mutably } impl Rectangle { // You can have multiple impl blocks fn square(size: f64) -> Self { Rectangle { width: size, height: size } } } let rect = Rectangle { width: 10., height: 5.} let area = rect.area() let square = Rectangle::square(7.); let (h,w) = square.to_tuple(); // value moved here; square left uninitialized Just like functions, methods can take ownership of “self” or borrow it im/mutably. Method call—Note: Rust doesn’t have an equivalent to the -> operator in C++; instead, it provides automatic de/referencing of pointers when calling methods. R. Casadei Intro Ownership, borrows, moves UDTs More 38/61
  39. 39. Generic structs Generic structs: accept type parameters pub struct Queue<T> { older: Vec<T>, younger: Vec<T> } impl<T> Queue<T> { pub fn new() -> Self { // return type in place of Queue<T> Queue { older: Vec::new(), younger: Vec::new() } } pub fn push(&mut self, t: T) { self.younger.push(t); } } For static method calls, you can supply the type parameter explicitly using the turbofish ::<> notation: let mut q = Queue::<char>::new(); But in practice, you can usually just let Rust figure it out for you let mut q = Queue::new(); q.push("CAD"); // apparently a Queue<&'static str> R. Casadei Intro Ownership, borrows, moves UDTs More 39/61
  40. 40. Enums While structs are “AND” (product) types, enums are “OR” (sum) types. Algebraic Data Types (union types) and pattern matching and if-let #[derive(Debug,Copy)] enum Msg { // Rust enums can contain various kinds of data Quit, // variant with no data (corresp. to unit-like structs) Move { x: i32, y: i32 }, // struct variant Write(String), // tuple variant ChangeColor(i32,i32,i32,i32), // tuple variant } impl Msg { // Like structs, enums can have methods fn m(self) -> &'static str { match self { Msg::Quit => "...", /* ... */ } } } let m = Msg::Write(String::from("hello")); if let Msg::Write(theMsg) = &m { println!("if-let") } else { }; // borrows 'm' let str = match m { // takes ownership of 'm' Msg::Write(s) => s, _ => String::from("dunno") // Match must be exhaustive }; Enums are useful whenever a value might be either one thing or another; the “price” of using them is that you must access the data safely, using pattern matching. In memory: enums with data are stored as a small integer tag, plus enough memory to hold all the fields of the largest variant Enums provide safety for flexibility: end users of an enum can’t extend it to add new variants; variants can be added only by changing the enum declaration (and when that happens, existing code breaks—new variants must be dealt with via new match arms). R. Casadei Intro Ownership, borrows, moves UDTs More 40/61
  41. 41. Traits A trait is a feature that any given type may or may not support (≈ typeclass) A value that impls std::io::Write can write out bytes A value that impls std::iter::iterator can produce a seq of values trait Write { fn write(&mut self, buf: &[u8]) -> Result<usize>; fn flush(&mut self) -> Result<()>; fn write_all(&mut self, buf: &[u8]) -> Result<()> { /*...*/ } //... } use std::io::Write; fn say_hello(out: &mut Write) -> std::io::Result<()> { out.write_all(b"hello worldn")?; out.flush() } // &mut Write => "a mutable ref to any value that impls trait Write" let mut local_file = std::fs::File::create("hello.txt")?; say_hello(&mut local_file)?; // works let mut bytes = vec![]; say_hello(&mut bytes)?; // also works assert_eq!(bytes, b"hello worldn"); fn min<T: Ord>(v1: T, v2: T) -> T { if v1<=v2 { v1 } else { v2 } } // BOUND <T: Ord> means "this fun can be used with any T that impls trait Ord" As can extend any type, a trait must be in scope to be used. Traits like e.g. Clone, are in std prelude and hence automatically imported in any module. R. Casadei Intro Ownership, borrows, moves UDTs More 41/61
  42. 42. Trait objects: references to traits You can’t have vars typed Write: a var’s size must be known at compile time, and types that impl Write can be any size. use std::io::Write; let mut buf: Vec<u8> = vec![]; let writer: Write = buf; // error: `Write` does not have a constant size Instead, you need a trait object, i.e., a reference to a trait type. let writer: &mut Write = &mut buf; // ok Trait object layout: a fat pointer with (1) a pointer to a value, (2) a pointer to a table representing that value’s type (so it takes up to 2 machine words) In Rust, as in C++, the vtable is generated once (statically) and shared by all objects of the same type. While C++ stores vptras part of the struct, Rust uses fat pointers (the struct itself contains nothing but fields) So, a struct can impl many traits w/o containing many vptrs. Rust automatically converts ordinary refs into trait objects (or among fat pointers, e.g., from Box<File> to Box<Write>) when needed. R. Casadei Intro Ownership, borrows, moves UDTs More 42/61
  43. 43. Generic functions and bounds fn hello_plain(out: &mut Write) -> Result<()> { ... } fn hello_gen<W: Write>(o: &mut W) -> Result<()> { o.write_all(b"Hellon")?; o.flush(); } hello_gen(&mut local_file)?; // calls say_hello::<File> hello_gen(&mut bytes)?; // calls say_hello::<Vec<u8>> W is a type parameter; with bound W: Write it stands for some type that impls Write. Rust generates machine code for concrete instantiations of generic functions, e.g., hello_gen::<File>() that calls appropriate File::write_all() and File::flush(). If we need multiple abilities from a type parameter, we can “concatenate” bounds with + fn top_ten<T: Debug + Hash + Eq>(values: &Vec<T>) { ... } No bounds: you can’t do much such a val: you can move it, put it into a box/vec.. That’s it. Multiple parameter types: fn run_query<M: Mapper + Serialize, R: Reducer + Serialize>( data: &DataSet, map: M, reduce: R) -> Results { ... } // Alternate syntax fn run_query<M, R>(data: &DataSet, map: M, reduce: R) -> Results where M: Mapper + Serialize, R: Reducer + Serialize { ... } Lifetime parameters: these come first fn nearest<'t,'c,P>(p: &'t P, pts: &'c [P]) -> &'c P where P: MeasureDist {...} R. Casadei Intro Ownership, borrows, moves UDTs More 43/61
  44. 44. Trait objects vs. generic code Trait objects: Trait objects are the right choice whenever you need a collection of values of mixed types, all together. trait Vegetable { ... } struct Salad<V: Vegetable>{ veggies: Vec<V> } // single type of vegetables struct Salad { veggies: Vec<Vegetable> } // ERR: `Vegetable` hasn't constant size struct Salad { veggies: Vec<Box<Vegetable>> } Each Box<Vegetable> can own any type of vegetable, but the box itself has a constant size (two pointers). Another possible reason to use trait objects is to reduce the total amount of compiled code. Rust may have to compile a generic function many times, once for each type it’s used with. Generics: Speed. Rust compiler generates machine code for generic functions, it knows the types it’s working with and hence which methods to call: so there’s no dynamic dispatch (unlike with trait objects). Inlining is possible, calls with consts can be evaluated at compile time etc. Moreover, not every trait can support trait objects. R. Casadei Intro Ownership, borrows, moves UDTs More 44/61
  45. 45. Traits: definition/impl (1/2) Trait declaration and implementation trait T { fn m(&self); fn n(&self){ self.m(); self.m(); } // default method } impl T for SomeType { // This block only contains impl of trait methods fn m(&self){ /*...*/ } } Extension trait: trait created with the sole purpose of adding a method to existing types. You can even use a generic impl block to impl a trait for a whole family of types at once. impl <W: Write> SomeTrait for W { ... } Coherence rule: when you impl a trait, the trait or type must be new in current crate It helps Rust ensure that trait implementations are unique. Subtrait—if trait B extends trait A, then all B values are also A values So, any type that impls B must also impl A trait B : A { ... } impl B for SomeType { ... } impl A for SomeType { ... } R. Casadei Intro Ownership, borrows, moves UDTs More 45/61
  46. 46. Traits: definition/impl (2/2) Self: a trait can use keyword Self as a type pub trait Clone { fn clone(&self): Self; /*...*/ } pub trait Spliceable { fn splice(&self, other: &Self) -> Self; /*...*/ } A trait that uses the Self type is incompatible with trait objects. // ERROR: trait 'Spliceable' cannot be made into an object fn splice_anything(a: &Spliceable, b: &Spliceable){ let combo = a.splice(b); Rust rejects this code cause it has no way to typecheck call a.splice(b) The whole point of trait objects is that the type isn’t known until runtime. Rust has no way to know at compile time if left and right will be the same type, as required. The more advanced features of traits are useful, but they can’t coexist with trait objects because with trait objects, you lose the type info Rust needs to type-check your program. − A trait-object-friendly trait for splicing must not use Self: pub trait MegaSpliceable { fn splice(&self, other: &MegaSpliceable) -> Box<MegaSpliceable>; } Trait objects don’t support static methods: if you want to use &StringSet trait objects, you must excuse these by adding bound where Self: Sized trait StringSet { fn new() -> Self where Self: Sized; R. Casadei Intro Ownership, borrows, moves UDTs More 46/61
  47. 47. Fully Qualified Method Calls A method is just a special kind of function "hello".to_string() // is equivalent to str::to_string("hello") // or ToString::to_string("hello") // since to_string is a method of trait ToString // or <str as ToString>::to_string("hello") All but the first are qualified method calls: they specify the type or trait that the method is associated with. The last one is fully qualified R. Casadei Intro Ownership, borrows, moves UDTs More 47/61
  48. 48. Traits that define relationships between types (1/2) Associated types (or How iterators work) pub trait Iterator { // Rust's standard Iterator trait type Item; // ASSOCIATED TYPE => it's is a feature of each type of iterator.. fn next(&mut self) -> Option<Self::Item>; // ..so we access it by Self::item } impl Iterator for Args { // std::env::args() returns an Args type Item = String; fn next(&mut self) -> Option<String> { ... } Associated types are types specified by trait implementations Generic code can use associated types fn collect_into_vector<I: Iterator>(iter: I) -> Vec<I::Item> { let mut res = Vec::new(); for v in iter { res.push(v); } res } fn dump<I>(it: I) where I: Iterator, I::Item: Debug { for (i,v) in it.enumerate(){ println!("{}: {:?}", i, v); } } // or also: fn dump<I>(it: I) where I: Iterator<Item=String> If you think of Iterator as the set of all iterator types, then Iterator<Item=String> is a subset of Iterator: the set of iterator types that produce Strings − Traits with associated types, like Iterator, are compatible with trait objects, but only if all the associated types are spelled out. R. Casadei Intro Ownership, borrows, moves UDTs More 48/61
  49. 49. Traits that define relationships between types (2/2) Generic traits (or How operator overloading works) In Rust, trait std::ops::Mul is for types that support multiplication * pub trait Mul<RHS=Self> { // Type param RHS defaults to Self type Output; // result type after applying op '*' fn mul(self, rhs: RHS) -> Self::Output; // method for '*' } impl Mul for Complex { ... } // eq. to: impl Mul<Complex> for Complex { .. } fn f<M:Mul>(){ ... } // eq. to: fn f<M:Mul<M>>(){ ... } Buddy traits (or How rand::random() works) “Buddy traits” are traits designed to work together pub trait Rng { fn next_u32(&mut self) -> u32; // ... } pub trait Rand: Sized { fn rand<R: Rng>(rng: &mut R) -> Self; } // NOTE: trait Rand uses Rng as bound for its method rand let x = f64::rand(rng); Another example is trait Hash for hashable types and trait Hasher for hashing algorithms. R. Casadei Intro Ownership, borrows, moves UDTs More 49/61
  50. 50. Reverse-engineering bounds Consider the following and suppose we want to make it generic to also support floats. fn dot(v1: &[i64], v2: &[i64]) -> i64 { let mut tot = 0; for i in 0 .. v1.len() { tot = tot + v1[i] * v2[i]; }; tot } With a boundless type parameter N, Rust would complain for uses of +,* and 0 You can introduce bound T: Add+Mul+Default This still wouldn’t work, because total=total+v1[i]*v2[i] assumes that multiplying two values of type N yields another N (which isn’t generally the case) You need to be specific about the output fn dot<N: Add<Output=N> + Mul<Output=N> + Default>(v1: &[N], v2: &[N]) - > N {..} // error[E0508]: cannot move out of type `[N]`, a non-copy array // total = total + v1[i] * v2[i]; // ^^^^^ cannot move out of here But it still complains: it would be illegal to move v1[i] out of the slice, but numbers are copyable, so what’s the problem? The point is that Rust doesn’t know v1[i] is a number and hence copyable. So the final, working bound is where N: Add<Output=N> + Mul<Output=N> + Default + Copy * Crate num defines a Num trait that would allow us to simplify the bound into where N: Num + Copy R. Casadei Intro Ownership, borrows, moves UDTs More 50/61
  51. 51. Rust generics vs. C++ templates One advantage of Rust approach vs. C++ templates (where constraints are left implicit in the code, à la duck typing) is forward compatibility of generic code (as long as you don’t change the signature, you’re fine) Another benefit is legibility and documentation Moreover, C++ compiler error messages involving templates can be much longer than Rust’s, pointing at many different lines of code, because the compiler has no way to tell who’s to blame for a problem: the template, its caller (which might also be a template), or that template’s caller. R. Casadei Intro Ownership, borrows, moves UDTs More 51/61
  52. 52. Traits for operator overloading (1/2) Traits for operator overloading are defined under std::ops Unary ops: -x (Neg, !x (Not) Arithmetic ops: +,-,*,/,% (Add, Sub, Mul, Div, Rem) Bitwise ops: &,|,^,<<,>> (BitAnd, BitOr, BitXor, Shl, Shr) Compound assignment/arithmetic ops: x+=y,... (AddAssign, SubAssign, ...) Compound assignment/bitwise ops: x&=y,... (BitAndAssign, ...) Indexing: x[y],&x[y] (Index), x[y]=z, &mut x[y] (IndexMut) Traits for comparison operator overloading are def under std::cmp Comparison: x==y, x!=y (PartialEq), x<y,... (PartialOrd) R. Casadei Intro Ownership, borrows, moves UDTs More 52/61
  53. 53. Traits for operator overloading (2/2) Example: Add trait Add<RHS=Self> { // std::ops::Add definition type Output; fn add(self, rhs: RHS) -> Self::Output; } // a+b is actually a shorthand for a.add(b) use std::ops::Add; // need to import for writing a.add(b) assert_eq!(4.124f32.add(5.75), 9.875); Example: operators for Complex #[derive(Clone,Copy,Debug)] struct Complex { re: T, im: T } impl<T> Add for Complex<T> where T: Add<Output=T> { type Output = Self; fn add(self, rhs: Self): -> Self { Complex{ re:self.re+rhs.re, im:self.im+rhs.im }} } // notice that operands are taken by value // We may loose constraints to allow diff types for + and diff result type impl<L, R, O> Add<Complex<R>> for Complex<L> where L: Add<R, Output=O> { type Output = Complex<O>; fn add(self, rhs: Complex<R>) -> Self::Output { ... } } // However, may not be much more useful that the simpler generic def impl<T> AddAssign for Complex<T> where T: AddAssign<T> { fn add_assign(&mut self, rhs: Complex<T>){ self.re+=rhs.re; self.im+=rhs.im; } } // for c1 += c2 R. Casadei Intro Ownership, borrows, moves UDTs More 53/61
  54. 54. Closures » Types and Safety (1/2) Function vs. closure type Function type: fn(ArgT1,ArgT2,...)->RetType Return type is optional: if omitted, it’s () Closure type: since a closure may contain data, every closure has its own, ad-hoc type created by the compiler, large enough to hold that data. Not two closures have exactly the same type, but every closure impls trait Fn To write a function that accepts a function or closure, you’ve to use a Fn bound fn my_hof<F>(f: F) where F: Fn(&Vec<i32>) -> bool { ... } Closures and safety Most of the story is simply that when a closure is created, it either moves or borrows the captured variables. However, some consequences are not obvious, or what happens when a closure drops or modifies a captured value. R. Casadei Intro Ownership, borrows, moves UDTs More 54/61
  55. 55. Closures » Types and Safety (2/2) Closures dropping values let my_str = "hello".to_string(); let f = || drop(my_str); // let f2 = || drop(my_str); // ERROR: my_str moved due to use in closure f(); // ok f(); // ERROR: use of moved value (NOTE: this prevents double free error!) Rust knows the above closure f can’t be called twice fn call_twice<F>(closure: F) where F: Fn() { closure(); closure(); } let my_str = "hello".to_string(); let f = || drop(my_str); call_twice(f); // ERROR: expected a closure that implements the `Fn` trait, // but this closure only implements `FnOnce` Closures that drop values, like f, are not allowed to have Fn: they impl a less powerful trait FnOnce: the trait of closures that can be called once. Closures containing mutable data or mut references FnMut is for closures that write (they, e.g., are not safe to call from multiple threads) let mut i = 0; let inc = || { i += 1; /* borrows a mut ref to i */; println!("i is {}", i); }; call_twice(inc); // error => fix with: fn call_twice<F:FnMut()>(mut c: F) {...} R. Casadei Intro Ownership, borrows, moves UDTs More 55/61
  56. 56. Outline 1 Intro 2 Ownership, borrows, moves 3 UDTs 4 More R. Casadei Intro Ownership, borrows, moves UDTs More 56/61
  57. 57. Iterators An iterator is any value that impls trait std::iter::Iterator trait Iterator { type Item; fn next(&mut self) -> Option<Self::item>; // Many default methods: map, fold, ... } If there’s a natural way to iterate over a type, the type can impl std::iter::IntoIterator and we call such type an iterable trait IntoIterator where Self::IntoIter::Item == Self::Item { type Item; // type of the values produced type IntoIter: Iterator; // type of the iterator value fn into_iter(self) -> Self::IntoIter; } The stdlib provides a blanket impl of IntoIterator for every type that impls Iterator. Under-the-hood, every for loop is just syntactic sugar over IntoIterator/Iterator methods: let v = vec!["antimony", "arsenic", "aluminum", "selenium"]; // Iterable for el in &v { println!("{}", el); } // is a shorthand for: let mut iterator = (&v).into_iter(); while let Some(el) = iterator.next() { println!("{}", el); } R. Casadei Intro Ownership, borrows, moves UDTs More 57/61
  58. 58. Concurrency example Goal: unifying iterator pipelines and thread pipelines documents.into_iter() .map(read_whole_file) .errors_to(error_sender) // filter out error results .off_thread() // spawn a thread for the above work .map(make_single_file_index) .off_thread() // spawn another thread for stage 2 ........... use std::thread::spawn; use std::sync::mpsc; // multiple producers, single consumer pub trait OffThreadExt: Iterator { fn off_thread(self) -> mpsc::IntoIter<Self::Item>; } impl<T> OffThreadExt for T where T: Iterator+Send+'static, T::Item: Send+'static { fn off_thread(self) -> mpsc::IntoIter<Self::Item> { let (snd,recvr) = mpsc::sync_channel(1024); // to transfer items from worker thread spawn(move || { // Move this iterator to a new worker thread and run it there for item in self { if snd.send(item).is_err() { break; } } }); recvr.into_iter() // return an iterator that pulls vals from the channel } } Channels are one-way conduits for sending values from a thread to another Sync channels support backpressure by blocking send()s if the channel is full Vals of Send types are safe to be passed as values (i.e. moved) across threads mpsc::IntoIter (created by Receiver::into_iter) is an iterator over msgs on a Receiver, which block whenever next is called, waiting for a new msg. R. Casadei Intro Ownership, borrows, moves UDTs More 58/61
  59. 59. Macros (1/2) Goal // Given #[derive(Clone,PartialEq,Debug)] enum Json { Null, Bool(bool), Num(f64), Str(String), Arr(Vec<Json>), Obj(Box<HashMap<String,Json>>) // This... let students = json!([ { "name": "Bob", "class_of": 1926, "major":"CS" }, { "name": "Steve", "class_of": 1933, "major":"Ph" } ]); // Should expand to... let students = Json::Array(vec![ Json::Object(Box::new(............. R. Casadei Intro Ownership, borrows, moves UDTs More 59/61
  60. 60. Macros (2/2) Basic solution macro_rules! json { (null) => { // (pattern) => (template) Json::Null }; ([ $( $elem:tt ),* ]) => { // Handle array Json::Array(vec![ $( json!($elem) ),* ]) // Notice recursion! }; ({ $( $key:tt : $value:tt ),* }) => { // Handle object Json::Object(Box::new(vec![ $( ($key.to_string(), json!($value)) ),*] .into_iter().collect())) }; ($other:tt) => { // $other is a fragment of type 'tt' (token tree) Json::from($other) // Handle Boolean/number/string }; } macro_rules! impl_from_num_for_json { ( $( $t:ident )* ) => { // fragment $t is an identifier $( impl From<$t> for Json { fn from(n: $t) -> Json { Json::Number(n as f64) } } )* }; } impl_from_num_for_json!(u8 i8 u16 i16 u32 i32 u64 i64 usize isize f32 f64); R. Casadei Intro Ownership, borrows, moves UDTs More 60/61
  61. 61. References (1/1) [1] J. Blandy and J. Orendorff. Programming Rust: Fast, Safe Systems Development. O’Reilly Media, 2017. ISBN: 9781491927236. URL: https://books.google.es/books?id=hcc_DwAAQBAJ. R. Casadei Appendix References 61/61

×