SlideShare a Scribd company logo
1 of 61
Download to read offline
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
Outline
1 Intro
2 Ownership, borrows, moves
3 UDTs
4 More
R. Casadei Intro Ownership, borrows, moves UDTs More 2/61
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
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
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
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
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
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
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
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
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
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
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
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
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
Outline
1 Intro
2 Ownership, borrows, moves
3 UDTs
4 More
R. Casadei Intro Ownership, borrows, moves UDTs More 16/61
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Outline
1 Intro
2 Ownership, borrows, moves
3 UDTs
4 More
R. Casadei Intro Ownership, borrows, moves UDTs More 36/61
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Outline
1 Intro
2 Ownership, borrows, moves
3 UDTs
4 More
R. Casadei Intro Ownership, borrows, moves UDTs More 56/61
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
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
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
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
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

More Related Content

What's hot

Rust-lang
Rust-langRust-lang
C++ idioms by example (Nov 2008)
C++ idioms by example (Nov 2008)C++ idioms by example (Nov 2008)
C++ idioms by example (Nov 2008)
Olve Maudal
 

What's hot (20)

Rust-lang
Rust-langRust-lang
Rust-lang
 
Rust vs C++
Rust vs C++Rust vs C++
Rust vs C++
 
Introduction to rust: a low-level language with high-level abstractions
Introduction to rust: a low-level language with high-level abstractionsIntroduction to rust: a low-level language with high-level abstractions
Introduction to rust: a low-level language with high-level abstractions
 
Rust system programming language
Rust system programming languageRust system programming language
Rust system programming language
 
Why Rust? - Matthias Endler - Codemotion Amsterdam 2016
Why Rust? - Matthias Endler - Codemotion Amsterdam 2016Why Rust? - Matthias Endler - Codemotion Amsterdam 2016
Why Rust? - Matthias Endler - Codemotion Amsterdam 2016
 
Rust: Systems Programming for Everyone
Rust: Systems Programming for EveryoneRust: Systems Programming for Everyone
Rust: Systems Programming for Everyone
 
Introduce to Rust-A Powerful System Language
Introduce to Rust-A Powerful System LanguageIntroduce to Rust-A Powerful System Language
Introduce to Rust-A Powerful System Language
 
The Rust Programming Language
The Rust Programming LanguageThe Rust Programming Language
The Rust Programming Language
 
Rust Tutorial | Rust Programming Language Tutorial For Beginners | Rust Train...
Rust Tutorial | Rust Programming Language Tutorial For Beginners | Rust Train...Rust Tutorial | Rust Programming Language Tutorial For Beginners | Rust Train...
Rust Tutorial | Rust Programming Language Tutorial For Beginners | Rust Train...
 
Rust
RustRust
Rust
 
Rust Primer
Rust PrimerRust Primer
Rust Primer
 
Scala Intro
Scala IntroScala Intro
Scala Intro
 
Go Lang Tutorial
Go Lang TutorialGo Lang Tutorial
Go Lang Tutorial
 
Java Deserialization Vulnerabilities - The Forgotten Bug Class (DeepSec Edition)
Java Deserialization Vulnerabilities - The Forgotten Bug Class (DeepSec Edition)Java Deserialization Vulnerabilities - The Forgotten Bug Class (DeepSec Edition)
Java Deserialization Vulnerabilities - The Forgotten Bug Class (DeepSec Edition)
 
Introduction to GoLang
Introduction to GoLangIntroduction to GoLang
Introduction to GoLang
 
C++ idioms by example (Nov 2008)
C++ idioms by example (Nov 2008)C++ idioms by example (Nov 2008)
C++ idioms by example (Nov 2008)
 
Guaranteeing Memory Safety in Rust
Guaranteeing Memory Safety in RustGuaranteeing Memory Safety in Rust
Guaranteeing Memory Safety in Rust
 
Effective testing with pytest
Effective testing with pytestEffective testing with pytest
Effective testing with pytest
 
File system node js
File system node jsFile system node js
File system node js
 
Golang 101
Golang 101Golang 101
Golang 101
 

Similar to The Rust Programming Language: an Overview

Os Vanrossum
Os VanrossumOs Vanrossum
Os Vanrossum
oscon2007
 
golang_refcard.pdf
golang_refcard.pdfgolang_refcard.pdf
golang_refcard.pdf
Spam92
 
Степан Кольцов — Rust — лучше, чем C++
Степан Кольцов — Rust — лучше, чем C++Степан Кольцов — Rust — лучше, чем C++
Степан Кольцов — Rust — лучше, чем C++
Yandex
 
Rust: код может быть одновременно безопасным и быстрым, Степан Кольцов
Rust: код может быть одновременно безопасным и быстрым, Степан КольцовRust: код может быть одновременно безопасным и быстрым, Степан Кольцов
Rust: код может быть одновременно безопасным и быстрым, Степан Кольцов
Yandex
 

Similar to The Rust Programming Language: an Overview (20)

Os Vanrossum
Os VanrossumOs Vanrossum
Os Vanrossum
 
Rust Workshop - NITC FOSSMEET 2017
Rust Workshop - NITC FOSSMEET 2017 Rust Workshop - NITC FOSSMEET 2017
Rust Workshop - NITC FOSSMEET 2017
 
golang_refcard.pdf
golang_refcard.pdfgolang_refcard.pdf
golang_refcard.pdf
 
Briefly Rust
Briefly RustBriefly Rust
Briefly Rust
 
C++11
C++11C++11
C++11
 
Nell’iperspazio con Rocket: il Framework Web di Rust!
Nell’iperspazio con Rocket: il Framework Web di Rust!Nell’iperspazio con Rocket: il Framework Web di Rust!
Nell’iperspazio con Rocket: il Framework Web di Rust!
 
Introduction to Rust
Introduction to RustIntroduction to Rust
Introduction to Rust
 
Introduction to Dart
Introduction to DartIntroduction to Dart
Introduction to Dart
 
Briefly Rust - Daniele Esposti - Codemotion Rome 2017
Briefly Rust - Daniele Esposti - Codemotion Rome 2017Briefly Rust - Daniele Esposti - Codemotion Rome 2017
Briefly Rust - Daniele Esposti - Codemotion Rome 2017
 
Rust and Eclipse
Rust and EclipseRust and Eclipse
Rust and Eclipse
 
Степан Кольцов — Rust — лучше, чем C++
Степан Кольцов — Rust — лучше, чем C++Степан Кольцов — Rust — лучше, чем C++
Степан Кольцов — Rust — лучше, чем C++
 
为什么 rust-lang 吸引我?
为什么 rust-lang 吸引我?为什么 rust-lang 吸引我?
为什么 rust-lang 吸引我?
 
Summary of C++17 features
Summary of C++17 featuresSummary of C++17 features
Summary of C++17 features
 
The Style of C++ 11
The Style of C++ 11The Style of C++ 11
The Style of C++ 11
 
Sysprog17
Sysprog17Sysprog17
Sysprog17
 
Socket Programming Intro.pptx
Socket  Programming Intro.pptxSocket  Programming Intro.pptx
Socket Programming Intro.pptx
 
Rust: код может быть одновременно безопасным и быстрым, Степан Кольцов
Rust: код может быть одновременно безопасным и быстрым, Степан КольцовRust: код может быть одновременно безопасным и быстрым, Степан Кольцов
Rust: код может быть одновременно безопасным и быстрым, Степан Кольцов
 
Namespaces
NamespacesNamespaces
Namespaces
 
Idiomatic C++
Idiomatic C++Idiomatic C++
Idiomatic C++
 
Swift Introduction
Swift IntroductionSwift Introduction
Swift Introduction
 

More from Roberto Casadei

Self-Organisation Programming: a Functional Reactive Macro Approach (FRASP) [...
Self-Organisation Programming: a Functional Reactive Macro Approach (FRASP) [...Self-Organisation Programming: a Functional Reactive Macro Approach (FRASP) [...
Self-Organisation Programming: a Functional Reactive Macro Approach (FRASP) [...
Roberto Casadei
 
Programming Distributed Collective Processes for Dynamic Ensembles and Collec...
Programming Distributed Collective Processes for Dynamic Ensembles and Collec...Programming Distributed Collective Processes for Dynamic Ensembles and Collec...
Programming Distributed Collective Processes for Dynamic Ensembles and Collec...
Roberto Casadei
 
Introduction to the 1st DISCOLI workshop on distributed collective intelligence
Introduction to the 1st DISCOLI workshop on distributed collective intelligenceIntroduction to the 1st DISCOLI workshop on distributed collective intelligence
Introduction to the 1st DISCOLI workshop on distributed collective intelligence
Roberto Casadei
 
Digital Twins, Virtual Devices, and Augmentations for Self-Organising Cyber-P...
Digital Twins, Virtual Devices, and Augmentations for Self-Organising Cyber-P...Digital Twins, Virtual Devices, and Augmentations for Self-Organising Cyber-P...
Digital Twins, Virtual Devices, and Augmentations for Self-Organising Cyber-P...
Roberto Casadei
 
Augmented Collective Digital Twins for Self-Organising Cyber-Physical Systems
Augmented Collective Digital Twins for Self-Organising Cyber-Physical SystemsAugmented Collective Digital Twins for Self-Organising Cyber-Physical Systems
Augmented Collective Digital Twins for Self-Organising Cyber-Physical Systems
Roberto Casadei
 
Tuple-Based Coordination in Large-Scale Situated Systems
Tuple-Based Coordination in Large-Scale Situated SystemsTuple-Based Coordination in Large-Scale Situated Systems
Tuple-Based Coordination in Large-Scale Situated Systems
Roberto Casadei
 
Collective Adaptive Systems as Coordination Media: The Case of Tuples in Spac...
Collective Adaptive Systems as Coordination Media: The Case of Tuples in Spac...Collective Adaptive Systems as Coordination Media: The Case of Tuples in Spac...
Collective Adaptive Systems as Coordination Media: The Case of Tuples in Spac...
Roberto Casadei
 
Engineering Resilient Collaborative Edge-enabled IoT
Engineering Resilient Collaborative Edge-enabled IoTEngineering Resilient Collaborative Edge-enabled IoT
Engineering Resilient Collaborative Edge-enabled IoT
Roberto Casadei
 

More from Roberto Casadei (20)

Programming (and Learning) Self-Adaptive & Self-Organising Behaviour with Sca...
Programming (and Learning) Self-Adaptive & Self-Organising Behaviour with Sca...Programming (and Learning) Self-Adaptive & Self-Organising Behaviour with Sca...
Programming (and Learning) Self-Adaptive & Self-Organising Behaviour with Sca...
 
A Presentation of My Research Activity
A Presentation of My Research ActivityA Presentation of My Research Activity
A Presentation of My Research Activity
 
Self-Organisation Programming: a Functional Reactive Macro Approach (FRASP) [...
Self-Organisation Programming: a Functional Reactive Macro Approach (FRASP) [...Self-Organisation Programming: a Functional Reactive Macro Approach (FRASP) [...
Self-Organisation Programming: a Functional Reactive Macro Approach (FRASP) [...
 
Programming Distributed Collective Processes for Dynamic Ensembles and Collec...
Programming Distributed Collective Processes for Dynamic Ensembles and Collec...Programming Distributed Collective Processes for Dynamic Ensembles and Collec...
Programming Distributed Collective Processes for Dynamic Ensembles and Collec...
 
Towards Automated Engineering for Collective Adaptive Systems: Vision and Res...
Towards Automated Engineering for Collective Adaptive Systems: Vision and Res...Towards Automated Engineering for Collective Adaptive Systems: Vision and Res...
Towards Automated Engineering for Collective Adaptive Systems: Vision and Res...
 
Aggregate Computing Research: an Overview
Aggregate Computing Research: an OverviewAggregate Computing Research: an Overview
Aggregate Computing Research: an Overview
 
Introduction to the 1st DISCOLI workshop on distributed collective intelligence
Introduction to the 1st DISCOLI workshop on distributed collective intelligenceIntroduction to the 1st DISCOLI workshop on distributed collective intelligence
Introduction to the 1st DISCOLI workshop on distributed collective intelligence
 
Digital Twins, Virtual Devices, and Augmentations for Self-Organising Cyber-P...
Digital Twins, Virtual Devices, and Augmentations for Self-Organising Cyber-P...Digital Twins, Virtual Devices, and Augmentations for Self-Organising Cyber-P...
Digital Twins, Virtual Devices, and Augmentations for Self-Organising Cyber-P...
 
FScaFi: A Core Calculus for Collective Adaptive Systems Programming
FScaFi: A Core Calculus for Collective Adaptive Systems ProgrammingFScaFi: A Core Calculus for Collective Adaptive Systems Programming
FScaFi: A Core Calculus for Collective Adaptive Systems Programming
 
6th eCAS workshop on Engineering Collective Adaptive Systems
6th eCAS workshop on Engineering Collective Adaptive Systems6th eCAS workshop on Engineering Collective Adaptive Systems
6th eCAS workshop on Engineering Collective Adaptive Systems
 
Augmented Collective Digital Twins for Self-Organising Cyber-Physical Systems
Augmented Collective Digital Twins for Self-Organising Cyber-Physical SystemsAugmented Collective Digital Twins for Self-Organising Cyber-Physical Systems
Augmented Collective Digital Twins for Self-Organising Cyber-Physical Systems
 
Tuple-Based Coordination in Large-Scale Situated Systems
Tuple-Based Coordination in Large-Scale Situated SystemsTuple-Based Coordination in Large-Scale Situated Systems
Tuple-Based Coordination in Large-Scale Situated Systems
 
Pulverisation in Cyber-Physical Systems: Engineering the Self-Organising Logi...
Pulverisation in Cyber-Physical Systems: Engineering the Self-Organising Logi...Pulverisation in Cyber-Physical Systems: Engineering the Self-Organising Logi...
Pulverisation in Cyber-Physical Systems: Engineering the Self-Organising Logi...
 
Collective Adaptive Systems as Coordination Media: The Case of Tuples in Spac...
Collective Adaptive Systems as Coordination Media: The Case of Tuples in Spac...Collective Adaptive Systems as Coordination Media: The Case of Tuples in Spac...
Collective Adaptive Systems as Coordination Media: The Case of Tuples in Spac...
 
Testing: an Introduction and Panorama
Testing: an Introduction and PanoramaTesting: an Introduction and Panorama
Testing: an Introduction and Panorama
 
On Context-Orientation in Aggregate Programming
On Context-Orientation in Aggregate ProgrammingOn Context-Orientation in Aggregate Programming
On Context-Orientation in Aggregate Programming
 
Engineering Resilient Collaborative Edge-enabled IoT
Engineering Resilient Collaborative Edge-enabled IoTEngineering Resilient Collaborative Edge-enabled IoT
Engineering Resilient Collaborative Edge-enabled IoT
 
Aggregate Processes in Field Calculus
Aggregate Processes in Field CalculusAggregate Processes in Field Calculus
Aggregate Processes in Field Calculus
 
AWS and Serverless Computing
AWS and Serverless ComputingAWS and Serverless Computing
AWS and Serverless Computing
 
Coordinating Computation at the Edge: a Decentralized, Self-organizing, Spati...
Coordinating Computation at the Edge: a Decentralized, Self-organizing, Spati...Coordinating Computation at the Edge: a Decentralized, Self-organizing, Spati...
Coordinating Computation at the Edge: a Decentralized, Self-organizing, Spati...
 

Recently uploaded

scipt v1.pptxcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...
scipt v1.pptxcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...scipt v1.pptxcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...
scipt v1.pptxcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...
HenryBriggs2
 
+97470301568>> buy weed in qatar,buy thc oil qatar,buy weed and vape oil in d...
+97470301568>> buy weed in qatar,buy thc oil qatar,buy weed and vape oil in d...+97470301568>> buy weed in qatar,buy thc oil qatar,buy weed and vape oil in d...
+97470301568>> buy weed in qatar,buy thc oil qatar,buy weed and vape oil in d...
Health
 

Recently uploaded (20)

Tamil Call Girls Bhayandar WhatsApp +91-9930687706, Best Service
Tamil Call Girls Bhayandar WhatsApp +91-9930687706, Best ServiceTamil Call Girls Bhayandar WhatsApp +91-9930687706, Best Service
Tamil Call Girls Bhayandar WhatsApp +91-9930687706, Best Service
 
Hazard Identification (HAZID) vs. Hazard and Operability (HAZOP): A Comparati...
Hazard Identification (HAZID) vs. Hazard and Operability (HAZOP): A Comparati...Hazard Identification (HAZID) vs. Hazard and Operability (HAZOP): A Comparati...
Hazard Identification (HAZID) vs. Hazard and Operability (HAZOP): A Comparati...
 
Bridge Jacking Design Sample Calculation.pptx
Bridge Jacking Design Sample Calculation.pptxBridge Jacking Design Sample Calculation.pptx
Bridge Jacking Design Sample Calculation.pptx
 
COST-EFFETIVE and Energy Efficient BUILDINGS ptx
COST-EFFETIVE  and Energy Efficient BUILDINGS ptxCOST-EFFETIVE  and Energy Efficient BUILDINGS ptx
COST-EFFETIVE and Energy Efficient BUILDINGS ptx
 
Design For Accessibility: Getting it right from the start
Design For Accessibility: Getting it right from the startDesign For Accessibility: Getting it right from the start
Design For Accessibility: Getting it right from the start
 
Navigating Complexity: The Role of Trusted Partners and VIAS3D in Dassault Sy...
Navigating Complexity: The Role of Trusted Partners and VIAS3D in Dassault Sy...Navigating Complexity: The Role of Trusted Partners and VIAS3D in Dassault Sy...
Navigating Complexity: The Role of Trusted Partners and VIAS3D in Dassault Sy...
 
scipt v1.pptxcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...
scipt v1.pptxcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...scipt v1.pptxcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...
scipt v1.pptxcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...
 
Learn the concepts of Thermodynamics on Magic Marks
Learn the concepts of Thermodynamics on Magic MarksLearn the concepts of Thermodynamics on Magic Marks
Learn the concepts of Thermodynamics on Magic Marks
 
Computer Lecture 01.pptxIntroduction to Computers
Computer Lecture 01.pptxIntroduction to ComputersComputer Lecture 01.pptxIntroduction to Computers
Computer Lecture 01.pptxIntroduction to Computers
 
AIRCANVAS[1].pdf mini project for btech students
AIRCANVAS[1].pdf mini project for btech studentsAIRCANVAS[1].pdf mini project for btech students
AIRCANVAS[1].pdf mini project for btech students
 
Air Compressor reciprocating single stage
Air Compressor reciprocating single stageAir Compressor reciprocating single stage
Air Compressor reciprocating single stage
 
Work-Permit-Receiver-in-Saudi-Aramco.pptx
Work-Permit-Receiver-in-Saudi-Aramco.pptxWork-Permit-Receiver-in-Saudi-Aramco.pptx
Work-Permit-Receiver-in-Saudi-Aramco.pptx
 
Thermal Engineering -unit - III & IV.ppt
Thermal Engineering -unit - III & IV.pptThermal Engineering -unit - III & IV.ppt
Thermal Engineering -unit - III & IV.ppt
 
data_management_and _data_science_cheat_sheet.pdf
data_management_and _data_science_cheat_sheet.pdfdata_management_and _data_science_cheat_sheet.pdf
data_management_and _data_science_cheat_sheet.pdf
 
HAND TOOLS USED AT ELECTRONICS WORK PRESENTED BY KOUSTAV SARKAR
HAND TOOLS USED AT ELECTRONICS WORK PRESENTED BY KOUSTAV SARKARHAND TOOLS USED AT ELECTRONICS WORK PRESENTED BY KOUSTAV SARKAR
HAND TOOLS USED AT ELECTRONICS WORK PRESENTED BY KOUSTAV SARKAR
 
+97470301568>> buy weed in qatar,buy thc oil qatar,buy weed and vape oil in d...
+97470301568>> buy weed in qatar,buy thc oil qatar,buy weed and vape oil in d...+97470301568>> buy weed in qatar,buy thc oil qatar,buy weed and vape oil in d...
+97470301568>> buy weed in qatar,buy thc oil qatar,buy weed and vape oil in d...
 
Unleashing the Power of the SORA AI lastest leap
Unleashing the Power of the SORA AI lastest leapUnleashing the Power of the SORA AI lastest leap
Unleashing the Power of the SORA AI lastest leap
 
Online food ordering system project report.pdf
Online food ordering system project report.pdfOnline food ordering system project report.pdf
Online food ordering system project report.pdf
 
A CASE STUDY ON CERAMIC INDUSTRY OF BANGLADESH.pptx
A CASE STUDY ON CERAMIC INDUSTRY OF BANGLADESH.pptxA CASE STUDY ON CERAMIC INDUSTRY OF BANGLADESH.pptx
A CASE STUDY ON CERAMIC INDUSTRY OF BANGLADESH.pptx
 
Computer Networks Basics of Network Devices
Computer Networks  Basics of Network DevicesComputer Networks  Basics of Network Devices
Computer Networks Basics of Network Devices
 

The Rust Programming Language: an Overview

  • 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. Outline 1 Intro 2 Ownership, borrows, moves 3 UDTs 4 More R. Casadei Intro Ownership, borrows, moves UDTs More 2/61
  • 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. Outline 1 Intro 2 Ownership, borrows, moves 3 UDTs 4 More R. Casadei Intro Ownership, borrows, moves UDTs More 16/61
  • 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. Outline 1 Intro 2 Ownership, borrows, moves 3 UDTs 4 More R. Casadei Intro Ownership, borrows, moves UDTs More 36/61
  • 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. Outline 1 Intro 2 Ownership, borrows, moves 3 UDTs 4 More R. Casadei Intro Ownership, borrows, moves UDTs More 56/61
  • 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. 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. 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. 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. 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