# Rust for Scalatians --- ## What is Rust? Blazingly fast, prevents segfaults, and guarantees thread safety (rust-lang.org) Safe, fast, productive -- pick three ([2017 vision](https://internals.rust-lang.org/t/setting-our-vision-for-the-2017-cycle/3958)) Note: Goal: familiarize and convince of the "safe, fast, productive" motto --- ## Pillars Memory safety without garbage collection Concurrency without data races Abstraction without overhead Stability without stagnation Note: Rust challenges these apparent contradictions --- ![How much you care](https://img.memecdn.com/how-much-i-care_o_508853.jpg) Note: I've probably talked about rust a few too many times --- ## Why learn Rust? - [FFI/JNI](https://github.com/Dushistov/rust_swig) for high perf components - Zero dependency cross-platform tools - We have [some] rust in prod - New paradigm (not really new research) --- ## Similarities to Scala Let's port some Scala. Note: Scala devs should be able to quickly feel familiar and productive with many aspects of Rust --- ### Immutable by default ```scala // Scala val x = 5 x = 10 // Error: reassignment to val var y = 5 y = 10 ``` ```rust // Rust let x = 5; x = 10; // Error: re-assignment of immutable variable let mut y = 5; y = 10; ``` Note: Minor syntax difference. Same idea. --- ## Type inferrence ```scala // Scala val greeting = s"Hello, $name" val greeting: String = s"Hello, $name" ``` ```rust // Rust let greeting = format!("Hello, {}", name); let greeting: String = format!("Hello, {}", name); ``` Note: Rust's type inferrence is *mostly* Hindly Milner Pythonistas might recognize `format!` syntax as inspired by: `'Hello, {}'.format(name)` --- ### but not inferred for functions ```scala // Scala def foo(arg: MyType) = arg.transform ``` ```rust // Rust fn foo(arg: MyType) -> MyOutput { arg.transform() } ``` Note: Some believe "explicit" is a core tenant of Rust; That's not quite accurate. "Predictable" is more accurate, and explicitness is often used to achieve predictability. --- ### Exhaustive Pattern Matching ```scala // Scala sealed abstract class Suit case class Club extends Suit case class Diamond extends Suit case class Heart extends Suit case class Spade extends Suit card.suit match { case Heart | Diamond => Color.Red case Club | Spade => Color.Black } ``` ```rust // Rust enum Suit { Heart, Club, Spade, Diamond } match card.suit() { Suit::Heart | Suit::Diamond => Color::Red, Suit::Club | Suit::Spade => Color::Black, } ``` Note: Enum's and exhaustive pattern matching are front-and-center in Rust --- ### [Dotty getting enums](https://github.com/lampepfl/dotty/issues/1970) ```scala // Scala enum Suit { case Heart, Club, Spade, Diamond } card.suit match { case Suit.Heart | Suit.Diamond => Color.Red case Suit.Club | Suit.Spade => Color.Black } ``` ```rust // Rust enum Suit { Heart, Club, Spade, Diamond } match card.suit() { Suit::Heart | Suit::Diamond => Color::Red, Suit::Club | Suit::Spade => Color::Black, } ``` Note: Scala will *catch up* with Rust on this front, soon. :-) --- ## Traits and Generics ```scala // Scala trait HasArea[T] { def area(): T } case class Square(side: Int) extends HasArea[Int] { def area() = side * side } ``` ```rust // Rust trait HasArea<T> { fn area(&self) -> T; } struct Square { side: u32 } impl HasArea<u32> for Square { fn area(&self) -> u32 { self.side * self.side } } ``` Note: Rust emphasizes "composition over inheritance" There is no "class" to extend, rather you "implement" traits for your type. --- ### Functional iterators and closures ```scala // Scala val these = Array(1,2,3,4,5) val those = Array(6,7,8,9,10) these.zip(those) .map{ case (i,j) => i*j } .sum ``` ```rust // Rust let these = vec![1,2,3,4,5]; let those = vec![6,7,8,9,10]; these.iter() .zip(those) .map(|(i,j)| i*j) .sum() ``` <p class="fragment">Abstractions without overhead</p> Note: Now that we're operating at this beautiful high-level of abstraction, let's pause to defend the "abstraction without overhead" claim. --- ### Zero overhead abstractions 10,000 elements in each array | Bench | Result | | ----- | ------ | | C (for loop -O2) | 6,029 ns/iter | | Rust (for loop) | 4,983 ns/iter | | Rust (functional) | 4,984 ns/iter | | Scala (for loop) | | | Scala (functional) | | [code](https://gist.github.com/anowell/9936f3b0f4ba0a8ad2c651bf9baf38ed) Note: Main idea: functional rust has no overhead compared to the for loop This is NOT a claim that Rust faster than C. -O3 flattened the C loop resulting in 4000 ns/iter. Rust and C are in a similar performance class. --- ### Zero overhead abstractions 10,000 elements in each array | Bench | Result | | ----- | ------ | | C (for loop -O2) | 6,029 ns/iter | | Rust (for loop) | 4,983 ns/iter | | Rust (functional) | 4,984 ns/iter | | Scala (for loop) | 20,467 ns/iter | | Scala (functional) | 697,099 ns/iter | [code](https://gist.github.com/anowell/9936f3b0f4ba0a8ad2c651bf9baf38ed) Note: Main idea: Scala functional has a 35x overhead compared to scala for loop This surprised me; am I supposed to choose idiomatic scala or 35x perf? Rust vs Scala perf is not the focus here. --- ### Futures ```scala // Scala implicit val ec = ExecutionContext.global val f = Future { session.getLatestPost } f onComplete { case Success(post) => println(post) case Failure(err) => println("Error: " + err.getMessage) } ``` ```rust // Rust (still early stage) let pool = CpuPool::new_num_cpus(); let f = pool.spawn_fn(|| { session.getLatestPost() }); f.then(|res| { match res { Ok(post) => println!("{}", post), Err(err) => println!("Error: {}", err), } }) ``` Note: Ton of work going into Rust's futures and async story including async/await features. --- ### Futures + Async I/O [Tokio](https://tokio.rs/) is a network application framework inspired by Twitter's [Finagle](https://twitter.github.io/finagle/guide/) A [benchmark top competitor](https://www.techempower.com/benchmarks/#section=data-r14&hw=ph&test=plaintext) (standard benchmarking disclaimers apply) Note: Tokio exemplifies and pushes the bounds of "abstraction without overhead" Should be very interesting to watch this space evolve --- <img src="http://i.imgur.com/8acN5PG.png" width=250> What's the catch? --- ### FizzBuzz Your first [impl of FizzBuzz might not work](https://chrismorgan.info/blog/rust-fizzbuzz.html). ```rust for i in 1..100 { let res = if i % 15 == 0 { "fizzbuzz" } else if i % 3 == 0 { "fizz" } else if i % 5 == 0 { "buzz" } else { i.to_string() }; println!("{}", res); } ``` <p class="fragment">error: if and else have incompatible types</p> --- ### String vs str | Type | Description | | ---- | -------------- | | String | Heap-allocated, growable, UTF-8 string | | &str | Reference to UTF-8 "string slice" (could point on heap, stack, or static memory) | -- `"fizzbuzz"` is a reference to static memory hard-coded in the binary `i.to_string()` allocates a `String` on the heap Note: A good understanding of this problem requires a detour through Rust's Ownership & Borrowing system --- ### Ownership & Borrowing > "experienced Rust developers report that once they work with the rules of the ownership system for a period of time, they **fight the borrow checker** less and less." Note: Reading between the lines: inexperienced Rust developers fight the borrow checker Also good time to check for basic understanding of stack vs heap --- ### Copy types/semantics Special trait for automaically copied types ```rust fn add(a: u32, b: u32) -> u32 { a + b } fn main() { let a = 5; let b = 6; let c = 7; add(a, b); add(b, c); } ``` Simple types just work Note: this is often taught as the exception to the rule, but I'm starting with it since the behavior is familiar --- ### Move semantics (owned types) Most types "move" or "transfer ownership" ```rust fn concat(a: String, b: String) -> String { [a, b].concat() } fn main() { let greet = "Hello ".to_string(); let alice = "Alice".to_string(); let bob = "Bob".to_string(); concat(greet, alice); concat(greet, bob); } ``` <p class="fragment">error: use of moved value: `greet`</p> Note: Contrast this with previous slide. Code structure is the same, but `String` isn't a `Copy` type `greet` is moved into the first call to `concat` so it can't be used a 2nd time in `main` --- ### RAII for Owned types Resource Acquisition Is Initialization ```rust fn concat(a: String, b: String) -> String { [a, b].concat() } // 'a' and 'b' go out-of-scope and are freed fn main() { let greet = "Hello ".to_string(); let alice = "Alice".to_string(); let bob = "Bob".to_string(); concat(greet, alice); concat(greet, bob); } ``` Owned types dropped at end of scope Note: "dropped" means "freed" or "deallocated". "destructor" is defined by the `Drop` trait - seldom needs manually implemented RAII should be familiar to modern C++ devs --- ### Clone and move Instead "move" a "clone" (deep copy) ```rust fn concat(a: String, b: String) -> String { [a, b].concat() } fn main() { let greet = "Hello ".to_string(); let alice = "Alice".to_string(); let bob = "Bob".to_string(); concat(greet.clone(), alice); concat(greet, bob); } ``` Seriously? Note: `String` is a `Clone` type `Clone` implies an allocation is required Rust would NOT be such a fast language if allocating was the default answer to satisfying the borrow checker --- ### Shared references Borrow a shared (immutable) reference ```rust fn concat(a: &String, b: &String) -> String { [a, b].concat() } fn main() { let greet = "Hello ".to_string(); let alice = "Alice".to_string(); let bob = "Bob".to_string(); concat(&greet, &alice); concat(&greet, &bob); } ``` Better. Note: We'll revisit this example later - we can do better yet "Borrow" and "reference" are used interchangeably for `&` "shared reference" == "immutable reference" --- ### Mutable references Borrowing mutably ```rust fn increment_all(items: &mut Vec<u32>) { for item in items { *item += 1; } } fn main() { let mut counts = vec![1, 2, 3, 4]; increment_all(&mut counts); increment_all(&mut counts); } ``` Note: `*` is the "dereference" operator --- ### Borrow system rules 1. Single owner (move it and you lose it) 2. Multiple shared borrows xor single mutable borrow 3. Owned data must outlive any borrows Note: Once you internalize these rules, you'll find yourself "fighting the borrow checker" much less ...especially once NLL (non-lexical lifetimes) lands Didn't discuss #3 much, but basically you can't can't have a reference to a type that has already been dropped (e.g. you can't allocate a `String` and then return a reference to it, you'd have to return the owned `String`) --- Predictable, safe memory management without data races. But why were `"fizzbuzz"` and `i.to_string()` incompatible? Note: We finally have enough to understand Owned vs slice types. --- --- ### String vs str | Type | Description | | ---- | -------------- | | String | Heap-allocated, growable, UTF-8 string | | &str | Reference to UTF-8 "string slice" (could point on heap, stack, or static memory) | ### String vs str | Type | Description | | ---- | -------------- | | String | Heap-allocated, growable, UTF-8 string | | &str | Reference to UTF-8 "string slice" (could point on heap, stack, or static memory) | --- ### Visualizing Strings ![https://s3.amazonaws.com/anowell-public/rust-for-scalatians/string-str.png](https://s3.amazonaws.com/anowell-public/rust-for-scalatians/string-str.png) Note: That's 3 stack variables containing pointers to a single piece of memory on the heap `"Hello world".to_string();` would actually create a `String` with `cap=size=11` but I'm emphasizing that `cap` and `size` don't have to match --- ### String Conversions `String` to `&str` is free via 'Deref' operator ```rust let foo = "foo".to_string(); let bar = &*foo; let baz: &str = &foo; // automatic deref ``` -- &str to String requires an allocation ```rust let a: &str = "foo"; let b = foo.to_owned(); // via ToOwned trait let c = foo.to_string(); // via Display/ToString traits ``` Note: In practice, you rarely need `*` often because auto deref works in combination with type inference --- ### Revisiting shared borrow ```rust fn concat(a: &String, b: &String) -> String { [a, b].concat() } fn main() { let greet = "Hello ".to_string(); let alice = "Alice".to_string(); let bob = "Bob".to_string(); concat(&greet, &alice); concat(&greet, &bob); } ``` `&String` - were you going to read capacity? Note: `&String` is a pretty rare type to see since having acceess to capacity but no ability to modify isn't very interesting --- ### Revisiting shared borrow ```rust fn concat(a: &str, b: &str) -> String { [a, b].concat() } fn main() { let greet = "Hello ".to_string(); let alice = "Alice".to_string(); let bob = "Bob".to_string(); concat(&greet, &alice); concat(&greet, &bob); } ``` Use string slices (and auto `Deref`) Note: You can use `concat(&*greet, &*alice)`, but this is where auto-deref and type inference work together nicely But we can still do better.. --- ### Revisiting shared borrow ```rust fn concat(a: &str, b: &str) -> String { [a, b].concat() } fn main() { let greet = "Hello "; let alice = "Alice"; let bob = "Bob"; concat(greet, alice); concat(greet, bob); } ``` Now it also works with string literals Note: Hence it is more flexible to accept `&str` than `&String`: Of course, we still have to return a `String`, which effective moves ownership of the concatenated string back to the caller. --- ![http://anuragjain67.github.io/assets/img/post_thumbnails/confusing.jpg](http://anuragjain67.github.io/assets/img/post_thumbnails/confusing.jpg) [Maybe, someday](https://internals.rust-lang.org/t/pre-rfc-allowing-string-literals-to-be-either-static-str-or-string-similar-to-numeric-literals/5029) string literals could just work as `String`. Note: Realistically, owned vs sliced types are pretty fundamental to Rust --- | Owned Type | Sliced Type | | ----- | ------ | | String | str | | CString | Cstr | | OsString | OsStr | | PathBuf | Path | | Vec<T> | [T] | Note: `Vec<T>` and `[T]` are the fundamental types here, e.g., `String` is a specialized wrapper around `Vec<u8>` --- Is this borrow checker valuable? Note: So far this seems like ceremony, let's talk about why we tolerate this complexity: --- ### Iterate and append Let's append to a list while iterating over it. ```scala // Scala var items = Array(0, 1, 2, 3); for(item <- items) { items.push(item); } ``` What happens? --- ![Pure](http://stuff.thedeemon.com/lj/purefun.jpg) Runtime ConcurrentModificationException --- ### Iterate and append Let's append to a list while iterating over it. ```rust // Rust let mut items = vec![0, 1, 2, 3]; for item in items { items.push(item); } ``` <p class="fragment">Compile error: use of moved value: `items`</p> Note: 'for ... in items' allocates an iterator that takes ownership of items (to iterate over owned values) --- ### Iterate and append Iterate over borrows instead? ```rust // Rust let mut items = vec![0, 1, 2, 3]; for item in &items { items.push(*item); } ``` <p class="fragment">error: cannot borrow `items` as mutable because it is also borrowed as immutable</p> Note: 'for ... in &items' allocates an iterator that borrows items (to iterate over borrowed values) --- ### Concurrency ```rust let mut account = Account::new(); let t = thread::spawn(move || { let cached_balance = account.balance(); thread::sleep(Duration::from_secs(1)); account.set_balance(cached_balance + 1); println!("balance: {}", account.balance()); }); t.join().unwrap(); ``` > <!-- .element: class="fragment" --> balance: 1 [playground](https://is.gd/162lCt) --- ### Concurrency ```rust let mut account = Account::new(); let threads: Vec<_> = (0..10).map(|_| { thread::spawn(move || { let cached_balance = account.balance(); thread::sleep(Duration::from_secs(1)); account.set_balance(cached_balance + 1); println!("balance: {}", account.balance()); }) }).collect(); for t in threads { t.join().unwrap(); } ``` <p class="fragment">Violates single owner and use after move</p> [playground](https://is.gd/54xKVL) Note: Classic data race problem Can't move `account` into 10 different threads --- ### Concurrency ```rust let mut account = Account::new(); let threads: Vec<_> = (0..10).map(|_| { let account = &account; thread::spawn(move || { let cached_balance = account.balance(); thread::sleep(Duration::from_secs(1)); account.set_balance(cached_balance + 1); println!("balance: {}", account.balance()); }) }).collect(); for t in threads { t.join().unwrap(); } ``` <p class="fragment">Error: cannot borrow immutable borrowed content `*account` as mutable</p> [playground](https://is.gd/gotVF5) Note: While you can move 10 `&Account` references into 10 threads, `set_balance` requires a mutable reference (`&mut Account`) --- ### Concurrency ```rust let mut account = Account::new(); let threads: Vec<_> = (0..10).map(|_| { let account = &mut account; thread::spawn(move || { let cached_balance = account.balance(); thread::sleep(Duration::from_secs(1)); account.set_balance(cached_balance + 1); println!("balance: {}", account.balance()); }) }).collect(); for t in threads { t.join().unwrap(); } ``` <p class="fragment">Violates "single mutable borrow" rule</p> [playground](https://is.gd/gotVF5) Note: You can't have 10 `&mut Account` mutable references to `Account` --- No data races! ![https://i.imgflip.com/1otcw5.jpg](https://i.imgflip.com/1otcw5.jpg) --- ### Concurrency options - [Atomics](https://doc.rust-lang.org/std/sync/atomic/index.html), [Mutexes](https://doc.rust-lang.org/std/sync/struct.Mutex.html), [RwLocks](https://doc.rust-lang.org/std/sync/struct.RwLock.html) - [MPSC Channels](https://doc.rust-lang.org/std/sync/mpsc/index.html) - [Parallel work-stealing iterators](https://crates.io/crates/rayon) - [Futures](https://crates.io/crates/futures) with [threadpools](https://crates.io/crates/futures-cpupool) - etc.. Many concurrency abstractions that leverage the safety provided by `Send` and `Sync` Note: Hand-waving over solutions The language provides `Send` and `Sync` trait markers that form the foundation of an ecosystem with lots of safe concurrency abstractions --- Fast, safe, productive -- Pick three. How is Rust productive? ![https://media.licdn.com/mpr/mpr/shrinknp_400_400/p/1/005/073/2e3/22fd6b0.jpg](https://media.licdn.com/mpr/mpr/shrinknp_400_400/p/1/005/073/2e3/22fd6b0.jpg) --- ### Productive Rust - Iterators ✓ - Closures ✓ - Futures ✓ - Macros - Cargo - Error handling - Production Note: Want to cover a few more aspects of Rust that contribute to productivity --- ### Macros ```rust #[derive(Deserialize, Serialize)] struct Post { author: String, created_at: DateTime, related: Vec<Post> } json!({ "name": "Alice", "friends": ["John", "Carol", "Shakira"] }) ``` Boilerplate reduction Note: Macros make working with JSON almost as easy as in dynamic languages without sacrificing any of the type safety. --- ### Algorithmia Macro ```rust #[derive(Deserialize)] struct Input { name: String } #[entrypoint] fn apply(input: Input) -> Result<String, Error> { Ok(format!("Hello, {}", input.name)) } ``` Note: Our own macro for specifying algorithm entrypoint - not yet stable --- ### Cargo ```rust $ cargo new # stub out project $ cargo test # run tests $ cargo bench # run benchmarks $ cargo doc # doc generation $ cargo fmt # style correcting (rustfmt plugin) $ cargo clippy # linting (plugin) $ cargo install # install a rust binary $ cargo publish # publish to crates.io ``` and the entire crates.io ecosystem Note: a lot of productivity from one tool --- ### Cargo test/bench ```rust #[test] fn test_div_by_3() { assert_eq!(3/3, 1); } #[bench] fn bench_push(b: &mut Bencher) { let mut v = Vec::new(); b.iter(|| v.push(1)); } ``` --- ### Great documentation ```rust /// Builder method to configure the timeout in seconds /// /// # Examples /// /// ```no_run /// # use algorithmia::Algorithmia; /// let client = Algorithmia::client("sim123456"); /// client.algo("codeb34v3r/FindMinMax/0.1") /// .timeout(3) /// .pipe(vec![2,3,4]) /// .unwrap(); /// ``` pub fn timeout(&mut self, timeout: u32) -> &mut Algorithm { ``` [Docs](https://docs.rs/algorithmia/2.0.0/algorithmia/algo/struct.Algorithm.html#method.timeout) with examples that compile/run Automatic doc hosting on docs.rs --- ### Error handling ``` // Scala 1. try { } catch {} 2. Try[T] 3. Either[A, B] ``` ![Try conversation](https://s3.amazonaws.com/anowell-public/rust-for-scalatians/scala-errors.png) Note: Really too bad that Scala has to carry Java's baggage on error handling --- ### Result ```rust #[must_use] enum Result<T, E> { Ok(T), Err(E), } ``` ```rust let response = match algo.pipe(input) { Ok(response) => response, Err(err) => return Err(err.into()) } ``` Note: Sorta like Scala's `Either[A, B]` Notice the pattern of early returning on the `Err` case --- ### Try trait try! macro or `?` suffix operator ```rust let response = algo.pipe(input)?; ``` Chaining (crate) ```rust let response = algo.pipe(input) .chain_err(|| "Failed while calling anowell/pinky")?; ``` Note: Rich error handling that stays out of your way --- ### Production ![http://s2.quickmeme.com/img/64/64c954a02a84753908cb7e778ad16465063d2e2c7427b1df64a5c1ba3e36879d.jpg](http://s2.quickmeme.com/img/64/64c954a02a84753908cb7e778ad16465063d2e2c7427b1df64a5c1ba3e36879d.jpg) --- ![https://www.chromosphere.co.uk/wp-content/blogs.dir/1/files/2009/03/cost_curve1.jpg](https://www.chromosphere.co.uk/wp-content/blogs.dir/1/files/2009/03/cost_curve1.jpg) Note: Type system and borrow checker extending what Scala prevents to include: NPEs and data races Plus low barrier to entry to testing means testing picks up another good chunk --- Fast, safe, productive -- Pick three. --- What did I skip? - Lifetimes (most are elided) - Send + Sync - Closure types: Fn, FnMut, FnOnce - Associated types - Pointer types: Box, Rc, Arc - Interior mutability: Cell, RefCell, Mutex (All great topics for an "Effective Rust" follow up)
{"metaMigratedAt":"2023-06-14T12:47:19.643Z","metaMigratedFrom":"Content","title":"Rust for Scalatians","breaks":true,"contributors":"[{\"id\":\"885543c6-0e40-4a1e-b3f1-3319d1e6a26b\",\"add\":0,\"del\":9}]"}
    4259 views