Rust "Rust is for people who crave speed and stability in a language. By speed, we mean both how quickly Rust code can run and the speed at which Rust lets you write programs. The Rust compiler's checks ensure stability through feature additions and refactoring. This is in contrast to the brittle legacy code in languages without these checks, which developers are often afraid to modify. By striving for zero-cost abstractions, higher-level features that compile to lower-level code as fast as code written manually, Rust endeavors to make safe code be fast code as well." From the rust book. Go (in PL jargon): imperative statically scoped functions are / can be first class static variable types with type inference strongly typed Mostly pass by value. But a significant weirdness. value model of variables Special thing about Rust. No manual memory management (like C) AND no garbage collector (like Go/Java) HelloWorld.rs /* * Hello world in rust * Geoff Towell * Oct 2023 */ fn main() { // watch out for the "!", I constantly forget it println!("hello world"); // yes, a semi-colon } Compile: rustc HelloWorld.rs a 172 byte program gets compiled to 500,000ish bytes!! Run: ./HelloWorld Realistically, no one uses rustc directly Although "With simple projects, Cargo doesn’t provide a lot of value over just using rustc" cargo init hello_world_cargo ls -R hello_world_cargo Cargo.toml src/ HelloWorldC/src: main.rs main actually contains a hello world program!!! cd hello_world_cargo cargo run for the most part, rust does not like camelCase and will tell you about it. Mostly allowed, just frowned. Why??? Rust Convention Structs get camel case. Variables get snake case. Constants get all upper case. A great thread about these sort of choices in which the people who wrote rust got involved https://news.ycombinator.com/item?id=12532798 See message_rust Problem: Print a message — from the command line — a number of times — from the command line Go: message_go/message.go Rust cargo init message_rust Note: to get VSC to be most helpful, open a new window and in that window open the folder you just created. Also install the VSC extension "rust-analyzer" I have version 0.3.1689 first version fn main() { let args: Vec = env::args().collect(); println!("{} args\n", args); } the "::" denotes a package/module so here we are using the env package collect() — env:args() actually returns an iterator — come back to that — collect() essentially run through the iterator and collects up the iterated things into a Vec — equivalent to ArrayList in Java , slice in Go Vec — java-like generics let args — create a variable named args — default for variables in Rust is immutable println!("{} args\n"); — the {} is similar python, sort of equivalent to %v in Go printf Compiler fails "use of undeclared crate or module" Missing use std::env; Much like Go and java, need to tell rust about packages you will use. add that and still does not compile Error message: ^^^^ `Vec` cannot be formatted with the default formatter | = help: the trait `std::fmt::Display` is not implemented for `Vec` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead The rust compiler is very helpful. So first running version use std::env; fn main() { let args: Vec = env::args().collect(); println!("{:?} args\n", args); } Now: Convert second arg to integer fn main() { let args: Vec = env::args().collect(); //println!("{:#?} args\n", args); let guess = args[2].trim().parse::(); } this returns Result NOTE rust does type inference!!!! complaint :: looks like it indicates a module. To actually get the value: let val : u32; match guess { Ok(av) => { val = av; } Err(err) => { println!("{err}"); return; } } Question: Why the "av" variable??? What a pain. Realistically, everyone usually does: let guess:u32 = args[2].trim().parse().unwrap(); OR let vvvv = args[2].trim().parse::().unwrap(); But the match based system is the "correct" way of doing this. Unlike Go, Rust has lots of looping names let mut i=0; loop { println!("Loo {} {}", i, args[1]); i += 1; if i>val { break; } } i = 0; while i { print!("{gg} is too small"); lo=gg+1; }, Ordering::Greater => { print!("{gg} is too big"); hi=gg-1; }, Ordering::Equal => { println!("{gg} is just right"); return; } } println!(" range {lo}--{hi}"); } Variables: declare using "let" e.g. let ii : i32; Rust has type inference Integer type: i8, i16, i32, i64, i128, isize, u8, ..., usize isize is architecture dependent. On 64 bit it is i64 Why??? i32 is default (inferred type) handling integer overflow (debug vs release) Why different??? See program int_overflow/src/main.rs then: cargo build // creates executable in target/debug cargo build -r // creates executable in target/release Overflow check off in release. Read off end of array check still active in release! C does not check for read off end of array? Why??? floating point f32, f64 bool let bbb : bool = true; specified as using 1 byte. char: UTF-8 let c = 'z'; let z: char = 'ℤ'; tuple: Go has tuple assignment but no tuple type.Rust has one let tup = (500, 6.4, 1); // make a tuple let (x, y, z) = tup; // unpack a tuple arrays: like go they are a stack construct let a: [i32; 5] = [1, 2, 3, 4, 5]; runtime error if index is invalid. note: the type of an array is [baseType; length] numeric operations: type on sides must agree on type -- Go does this also ... Why??? -------------------------Finish 10/26 -------------------------------- named functions: Rust uses a UNIQUE pass-by-ownership, receive-by-ownership model for now, consider this as identical to pass by value default immutable to make mutable, must add mut to parameter list fn main() { println!("Hello, world!"); let bb = 5; receiver(bb); println!("{bb}"); } fn receiver(mut aa:i32) { println!("{aa}"); aa=9; println!("{aa}"); } fn r2s2(aa:i32, bb:i32) -> (i32, i32) { return (aa+5, bb+7) } NOTE::: for numbers the thing passed is a copy of the number so ownership is NOT transferred Will return to ownership blocks: scoping like Go BUT blocks also return a value -- if the last statement does not have a ";" let x = { let y = // get a random number y*y // NO semicolon } will assign to x the value of y*y What this means is that functions which return a value do NOT have to have explicit return. E.g., fn plus_one(x: i32) -> i32 { x + 1 } but if put ";" after "x+1" ERROR IS THIS A GOOD IDEA???? WHY does it exist??? Note that block returning also applies to functions (as they are a block) fn five_a() -> i32 { 5 } fn five_b() -> i32 { return 5; } Both of these are perfectly legal. casting and the power function for z in 0..=((max_val as f64).sqrt() as i32) for z in 0..=(f64::powf(max_val as f64, 1.0/4.0) as i32) ((max_val as f64).powf(1.0/4.0) as i32) personally I prefer Go syntax as there is no possible order of operations ambiguity. note that sqrt and pow both use very OO like syntax. can also be called in a functional way e.g. f64::sqrt(max_val as f64) finally, if that wasn't confusing enough casting via into() function f64::sqrt(max_val.into()) the compiler figures out what type is required (in this case f64) IF if returns a value!! SEE if_rust/src/main.rs use std::env; fn main() { let args: Vec = env::args().collect(); let val:u32 = args[1].trim().parse().unwrap(); let q = if val>9 {7} else { 8 }; println!("{q}"); println!("{}", if val>3 { 4} else {5}); } NOTE: without else this will not compile WHY??? That can return a value from if fall out of blocks returning values each block and be multi-line etc So, what happens if put ";" after hello??? NOTE NO ternary ? in Rust. Slice (vec): basically equivalent to Go slice let mut v: Vec = vec![1, 2, 3]; // note that rust can infer this type a struct (the struct is on the stack) that has a pointer into the heap unlike Go, vec.push() does not return a, possibly, new object. Instead it updates the pointer as needed. (The heap storage is admitted in Rust, it is hidden in Go.) Strings: rust has two strings: &str -- referred to as a "string literal" -- immutable String -- mutable SEE strings_rust/src/main use std::env; fn main() { let args: Vec = env::args().collect(); let val:u32 = args[1].trim().parse().unwrap(); let my_str = "hello"; // a string literal let mut my_string = String::from(my_str); let q = if val>9 { println!("{}",17); my_string.push_str("if side"); //change the string, but this is a statement that does not return a value my_string } else { println!("{}",42); my_string+"else side" // append using overloaded + }; println!("{q}") } Note a string is a slice, and can be used as such let s = String::from("hello"); let slice = &s[0..2]; let slice = &s[..2]; let len = s.len(); let esl = &s[2..len] let esl = &s[2..] Null Saftey in Rust: Rust will not let yo have a null pointer exception!!! 2 things: definite assignment -- done right Null save types -- ie types that allow you to work with something like null The principle one of these is "Option" This can contain either a value or None (ie a thing like a null pointer). use std::cmp; fn vec_min(v: &Vec) -> Option { let mut min = None; for e in 0..v.len() { min = Some(match min { None => v[e], Some(n) => cmp::min(n, v[e]) }); } println!("{:?}", min); min } fn main() { let v = vec![5,4,3,2,1]; let first = &v[0]; let vmm = vec_min(&v); println!("{:?}", vmm); let vmm = vec_min(&v); // NOTE WEIRD ... CAN REDEFINE VAR!! println!("{}", vmm.unwrap()); println!("The first element is: {}", *first); } SEE NULL_SAFTEY for the example I worked in class OWNERSHIP this is where rust gets really different "Keeping track of what parts of code are using what data on the heap, minimizing the amount of duplicate data on the heap, and cleaning up unused data on the heap so you don’t run out of space are all problems that ownership addresses." Ownership rules in rust mean that memory on the heap can be recovered (freed) without either garbage collection or manual deallocation statements. Ownership rules allow compiler to compute extent of every heap allocated bit of memory. Three Rules (to start, there are some more): 1. Each value in Rust has an owner. 2. There can only be one owner at a time. 3. When the owner goes out of scope, the value will be dropped. SEE owner/scr/main.rs fn main1() { let aa = "hello".to_string(); receiver(aa); //println!("{aa}"); } fn receiver(aa: String) { //aa.push_str("gg"); println!("{aa}"); } This will compile. However, uncomment line in receiver and it will not. must change "aa" to "mut aa" -- default immutability Uncomment line in main and will NOT. Problem, have ceded ownership of aa to the receiver function. So main can no longer use. Another example -- slight tweak on previous min finder here pass the vector rather than a pointer thereto fn vec_minn(v: Vec) -> Option { use std::cmp; let mut min = None; for e in 0..v.len() { min = Some(match min { None => v[e], Some(n) => cmp::min(n, v[e]) }); } println!("{:?}", min); min } fn main() { let v = vec![5,4,3,2,1]; let first = &v[0]; let vmm = vec_minn(v); // compile dies here!!!! first has a borrow of v, so main cannot give away ownership So delete "first" println!("{:?}", vmm); let vmm = vec_minn(v); // With first gone, compiler stops here, main no longer owns v!!! println!("{:?}", vmm.unwrap()); println!("The first element is: {}", *first); } Compiler: fn receiver(aa: String) { -------- ^^^^^^ this parameter takes ownership of the value On the other had, this works fn main2() { let aa = 5; receiver2(aa); println!("{aa}"); } fn receiver2(ra: i32) { println!("{ra}"); } It works because the following implement "copy". That is, they types do a pass by value just like Go All the integer types, such as u32. The Boolean type, bool, with values true and false. All the floating-point types, such as f64. The character type, char. Tuples, if they only contain types that also implement Copy. For example, (i32, i32) implements Copy, but (i32, String) does not. Quiz: in main3 what can be vars can printed at each of printlns A,B,..E and given that the var can be printed, what actually is printed? fn main3() { let s1 = 5; { println!("A .."); let s1 = g_o(); println!("B .."); let s2 = String::from("hello"); println!("C .."); let s3 = t_a_g(s2); println!("D .."); } println!("E .."); } fn g_o() -> String { let some_string = String::from("yours"); some_string } fn t_a_g(a_string: String) -> String { a_string } -----------------------Finished here 10/31------------------------------- Ownership rules allow compiler to compute extent of every heap allocated bit of memory. Three Rules (to start, there are some more): 1. Each value in Rust has an owner. 2. There can only be one owner at a time. 3. When the owner goes out of scope, the value will be dropped. Borrowing: Pass a reference into a func rather than the thing itself. BUT, since Rust is value model See image in pages point is that the reference is to the thing in the stack of the called function, not to the value itself. Every time you get a reference, you are borrowing: RUST RULE (#4): you may immutably borrow an infinite number of times. But you can only have one borrow if mutable! Pointer Safety Principle (the reason for rule #4): data should never be aliased and mutated at the same time (in any language). Chapter 4.5 is a nice overview with a rust-centric discussion of garbage collection, etc. SEE BORROWER_RUST fn main() { println!("Hello, world!"); let bb = 5; receiver(&bb); // borrow bb immutably println!("BB1 {bb}"); let mut bb = 5; // for next line to work, bb must be mutable. So redefine bb. What happens to first bb???? mut_receiver(&mut bb); //to allow a mutable borrow, must state explicitly in function that the thing being loaded is mutable. println!("BB2 {bb}"); } // here aa is defined to be mutable. BUT what is mutable? It is the pointer, NOT the value pointed at!!! fn receiver(mut aa:&i32) { println!("{aa}"); aa=&9; println!("{aa}"); } // here we borrow with a mutable pointer. This, we can change the value that is pointed at!! // must to be allow to do that, the function must identify the pointer as being mutable AND // the caller of the function must agree that the thing being borrowed is mutable fn mut_receiver(aa:&mut i32) { println!("{aa}"); *aa=9; println!("{aa}"); } Structs: SEE structs/ defintion is much like go method syntax is closer to OO struct pyramidsInEgypt { width : i32, breadth : i32, height : i32, name : String, extant : bool } #[derive(Debug)] Gets you something in {:?} // implement an interface -- in this case like toString impl fmt::Display for pyramidsInEgypt { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} has a base of {}x{} and height {} Exists:{} ", self.name, self.width, self.breadth, self.height, self.extant) } } Define methods impl pyramidsInEgypt { fn base_area(&self) -> i32 { self.breadth * self.width } fn volume(&self) -> f64 { return (self.width * self.breadth * self.height) as f64 / 3.0; } fn construct(b: i32,w: i32,h: i32) ->pyramidsInEgypt { pyramidsInEgypt { width: w, breadth: b, height: h, name: String::from("unnamed"), extant: false } } } can have lots of impl blocks construct method is weird because it does not have &self. So to call, can do pyramidsInEgypt::construct(10,10,10); Use: fn main() { println!("Hello, world!"); let g1 = pyramidsInEgypt{width:5, breadth: 6, height:7, name:String::from("Cheops"), extant:true}; println!("{}", g1); println!("{} volume is {}", g1.name, g1.volume()); } Note unlike Go 1. Must use named fields (constructor for defaults!!!) 2. Cannot leave any fields unset Rust is Null-Safe, this is part of the cost 3. Fields are immutable unless the instance of the struct itself is mutable. Mutability comes from the container Chapter 13: Functional Language Features anonymous functions let sqr = |x:i32| x*x; Anonymous functions are first class: In this example pass in / receive from, set to var ... fn main1() { let ff = ret_fun(5); let gg = |xx:i32| { println!("{}", ff(xx)); }; gg(20); rec_fun(ff, 10); rec_fun2(gg, 10); } fn ret_fun(tt:i32) -> fn(i32)->i32 { |x:i32| -> i32 { x*x} } fn rec_fun(fun:fn(i32)->i32, val: i32) { println!("{}", fun(val)); } fn rec_fun2(fun:fn(i32), val: i32) { fun(val); } BUT call to rec_fun2 fails with message: "closures can only be coerced to `fn` types if they do not capture any variables" Rust differentiates between a closure (which contains variables) and a function. Closures are allowed (and used) but the whole extent thing mucks with Rust ownership model. Hence the restriction on passed / received functions. So, anonymous functions are really constrained in Rust as they are usually NOT a closure (ie containing surrounding items.). The only time closures seems to be allowed is when the variable is within scope with the function. For instance: #[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; let mut num_sort_operations = 0; let comp = |r: &Rectangle| { num_sort_operations += 1; r.width }; list.sort_by_key(comp); list.sort_by_key(|r: &Rectangle| { num_sort_operations += 1; r.width }); println!("{:#?}, sorted in {num_sort_operations} operations", list); } Real action with anonymous functions is with iterators... -------------------------End of Rust discussion ----------------------------- ----------------------iterators are not on Midterm 2 ------------------------ Rust’s for loop syntax is actually sugar for iterators. let values = vec![1, 2, 3, 4, 5]; for x in values { println!("{x}"); } NOTE: using a for loop to iterate over a collection consumes that collection // iterators == doing things functionally fn main1000() { let vv: Vec = vec![1,2,3,4,5,6,7,8,9]; let mut summ = 0; // this is the imperative way of doing something functional vv.iter().for_each(|v| { summ = summ + v}); println!("Sum {}", summ); // this is the functional way -- the language defines a sum function summ = vv.iter().sum(); println!("Sum {}", summ); // functional way to sum squares -- first transform list into list of squares, then sum // note that map returns an iterator! // so to get the squares as a vector let sqs : Vec = vv.iter().map(|x| x*x).collect(); println!("{:?}", sqs); // another iterator function -- filtering. Keeps the things for which the boolean returns true. // trick, since the things are literally the same as in the original, we have an ownership problem. // two variables cannot own the same object. So after filter we call cloned() to make all new instances. // Again need collect() as the result of cloned() is an iterator. //(note copied() works as an alternative to cloned()). // What is difference between clone and copy) let evns : Vec = vv.iter().filter(|x: &&i32| *x%2==0).cloned().collect(); // a different approach to the double ownership problem. Here, "into_iter()" takes ownership of vv. So // after filtering there is only one owner of each object in the resulting vector (slice) let evns : Vec = vv.into_iter().filter(|x: &i32| *x%2==0).collect(); println!("filter {:?}", evns); // here 1..9 is an iterator, so we do not need the iter() function let summ_square : i32 = (1..9).map(|x| x*x).sum(); println!("SumSq {}", summ_square); // Question which is faster?? use std::time::Instant; let now: Instant = Instant::now(); let summ_square : i128 = (1..10000000).map(|x| x*x).sum(); let elapsed = now.elapsed(); println!("Elapsed map->sum: {:.2?} {}", elapsed, summ_square); let mut summ: i128 = 0; let now: Instant = Instant::now(); (1..10000000).for_each(|v| { summ = summ + v*v}); let elapsed = now.elapsed(); println!("Elapsed foreach: {:.2?} {}", elapsed, summ); } A full list of iterator functions is in https://doc.rust-lang.org/std/iter/index.html