## Intro to Rust * Michael Snoyman * VP of Software Development, FP Complete * Func Prog Sweden 2023 --- ## My quick background * Haskell developer for 15 years * Rust developer for 6 * Co-author of Begin Rust * Wrote the Rust Crash Course * Regularly train groups on beginner and intermediate Rust * And Haskell :) * Running multiple production apps in Rust: backend, blockchain, CLI, analytics, and more --- ## What is Rust? * Systems programming language * No garbage collector! Manual memory management * Competitor/replacement for C/C++ * Focus on high performance --- ## Isn't this an FP meetup? But Rust is much more * Strong types * Memory safety/no undefined behavior * Expression-oriented * Strong influences from FP languages * Zero-cost abstractions * "Fearless concurrency" * Great package management (cargo) * Macros to avoid boilerplate Point being: * Rust excels in systems programming * But also works great as an application language --- ## Hello World! No talk would be complete without it! ```rust fn main() { println!("Hello, world!"); } ``` * Generated automatically by `cargo new projectname` * What's the `!` for? Any guesses? <div class="fragment"> Rust uses macros throughout for better safety and convenience <pre><code class="lang-rust hljs">fn main() { println!("This is invalid syntax {"); println!("This variable doesn't exist {hello}"); let hello = Some(5); println!("This variable exists, but this still doesn't work: {hello}"); println!("But this works: {hello:?}"); println!("This too: {:?}", hello); }</code></pre> </div> --- ## Cargo.toml * We're going to use some extra dependencies in these slides * Want to follow along? Put this in your `Cargo.toml` * Brand new to Rust? Install at https://www.rust-lang.org/learn/get-started ```toml [package] name = "introtorust" version = "0.1.0" edition = "2021" [dependencies] anyhow = "1" axum = "0.6" clap = { version = "4", features = ["env", "derive"] } csv = "1" fs-err = "2" rand = "0.8" reqwest = { version = "0.11", features = ["json"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" tokio = { version = "1", features = ["full"] } [dev-dependencies] quickcheck = "1" ``` --- ## Basic flavor of the language * Let's start with some simple Rust examples * Once we get a flavor for the language, we'll dive into real stuff! ```rust use std::fmt::Display; struct Staff { name: String, job: Job, } enum Job { Principal, Teacher(Subject), } enum Subject { History, Physics, } fn main() { let staff = Staff { name: "Alice".to_string(), job: Job::Teacher(Subject::Physics), }; staff.greet(); } impl Staff { fn greet(&self) { println!("Hello {}, your job is {}", self.name, self.job); // Alternative with pattern matching/destructuring let Staff { name, job } = self; println!("Hello {name}, your job is {job}"); } } impl Display for Job { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Job::Principal => write!(f, "principal"), Job::Teacher(subject) => write!(f, "{subject} teacher"), } } } impl Display for Subject { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "{}", match self { Subject::History => "history", Subject::Physics => "physics", } ) } } ``` --- ## Iterators * Lends itself to function-style coding * Highly efficient generated code * Here's a silly example demonstrating it ```rust fn main() { (0..100) .filter(|x| x % 3 == 0) .map(|x| x + 1) .take_while(|x| *x < 50) .for_each(|x| println!("{x}")) } ``` --- ## Error handling * No runtime exceptions, errors are explicit * Leverages ADTs (`Result`) * `must_use` prevents you from ignoring an error * Great libraries like `anyhow` and `thiserror` make it convenient ```rust use std::io::{BufRead, BufReader}; use anyhow::{Context, Result}; fn main() -> Result<()> { let file = fs_err::File::open("Cargo.toml")?; let buffered_file = BufReader::new(file); for (idx, line_res) in buffered_file.lines().enumerate() { // Reading the line may have failed, handle that failure let line = line_res.context("Reading a single line failed")?; println!("Line #{idx}: {line}"); } Ok(()) // OK wrapping is a bit annoying, oh well :) } ``` --- ## Prime factors Let's do some math and write a test suite. ```rust fn read_line() -> String { let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); input } fn main() { println!("Enter an integer between 1 and {}", u16::MAX); let input = read_line(); let input = input.trim().parse().unwrap(); let factors = prime_factors(input); for factor in factors { println!("Prime factor: {factor}"); } } fn prime_factors(mut input: u16) -> Vec<u16> { if input == 0 { panic!("Input cannot be 0!"); } let mut factors = vec![]; let mut primes = vec![2]; let mut curr_prime = 2; while input != 1 { if input % curr_prime == 0 { factors.push(curr_prime); input /= curr_prime; } else { curr_prime = next_prime(curr_prime, &primes); primes.push(curr_prime); } } factors } fn next_prime(curr_prime: u16, primes: &[u16]) -> u16 { let is_prime = |candidate| { for prime in primes { if candidate % prime == 0 { return false; } else if *prime * *prime > candidate { return true; } } true }; for candidate in curr_prime + 1.. { if is_prime(candidate) { return candidate; } } panic!("Oops! We ran out of primes."); } #[cfg(test)] mod tests { use super::*; use quickcheck::quickcheck; #[test] fn empty_for_one() { assert_eq!(prime_factors(1), vec![]); } #[test] fn singleton_for_primes() { assert_eq!(prime_factors(2), vec![2]); assert_eq!(prime_factors(5), vec![5]); assert_eq!(prime_factors(7), vec![7]); assert_eq!(prime_factors(73), vec![73]); } #[test] fn spot_checks() { assert_eq!(prime_factors(4), vec![2, 2]); assert_eq!(prime_factors(8), vec![2, 2, 2]); assert_eq!(prime_factors(24), vec![2, 2, 2, 3]); assert_eq!(prime_factors(100), vec![2, 2, 5, 5]); assert_eq!(prime_factors(60), vec![2, 2, 3, 5]); } #[test] fn next_prime_examples() { assert_eq!(next_prime(3, &[2, 3]), 5); } quickcheck! { fn product_is_original(orig: u16) -> bool { orig == 0 || prime_factors(orig).into_iter().product::<u16>() == orig } } } ``` --- ## serde * **ser**ialize and **de**serialize data * Wonderful derive system with attributes * Support for _many_ different formats out of the box * Leveraged extensively through the ecosystem ```rust use anyhow::Result; use rand::Rng; #[derive(serde::Serialize, serde::Deserialize)] struct Point { x: i32, y: i32, } impl Point { fn random() -> Self { let mut rng = rand::thread_rng(); Point { x: rng.gen_range(-500..=500), y: rng.gen_range(-500..=500), } } } fn main() -> Result<()> { // Write 1000 random points to a CSV file let mut csv = csv::Writer::from_path("points.csv")?; for _ in 0..1000 { csv.serialize(Point::random())?; } csv.flush()?; // not needed, happens automatically on drop std::mem::drop(csv); // make sure the file is closed before opening it again // Now read the data into a Vec let points = csv::Reader::from_path("points.csv")? .into_deserialize() .collect::<Result<Vec<Point>, _>>()?; // And write the data to JSON and YAML formats let mut json = fs_err::File::create("points.json")?; serde_json::to_writer_pretty(&mut json, &points)?; let mut yaml = fs_err::File::create("points.yaml")?; serde_yaml::to_writer(&mut yaml, &points)?; Ok(()) } ``` --- ## Command line tooling * Wonderful library: clap * Heavy usage of derive macros and attributes * Leverages ADTs and traits ```rust use std::{fmt::Display, str::FromStr}; use clap::Parser; /// A simple program demonstrating how clap works #[derive(clap::Parser)] #[clap(author = "Michael Snoyman")] struct Opt { /// The name of the chat bot #[clap(long, global = true, env = "CHAT_BOT_NAME", default_value = "Eliza")] chat_bot_name: String, #[clap(subcommand)] cmd: Subcommand, } #[derive(clap::Parser)] enum Subcommand { SayHello { name: String, #[clap(long, default_value = "afternoon")] time: TimeOfDay, }, SayGoodbye, } #[derive(Clone, Copy)] enum TimeOfDay { Morning, Afternoon, Evening, } // Could use a helper crate like strum impl FromStr for TimeOfDay { type Err = anyhow::Error; fn from_str(s: &str) -> Result<Self, Self::Err> { match s { "morning" => Ok(TimeOfDay::Morning), "afternoon" => Ok(TimeOfDay::Afternoon), "evening" => Ok(TimeOfDay::Evening), _ => Err(anyhow::anyhow!("Unknown time of day: {s}")), } } } impl Display for TimeOfDay { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "{}", match self { TimeOfDay::Morning => "morning", TimeOfDay::Afternoon => "afternoon", TimeOfDay::Evening => "evening", } ) } } fn main() { let Opt { chat_bot_name, cmd } = Opt::parse(); match cmd { Subcommand::SayHello { name, time } => { println!("Good {time}, {name}. My name is {chat_bot_name}."); } Subcommand::SayGoodbye => println!("It's been a pleasure! I've been {chat_bot_name}"), } } ``` --- ## Web client * Leverages tokio and the async/await ecosystem * Direct support for JSON via serde * Provides a blocking API, would make more sense for this simple demo ```rust use anyhow::Result; #[tokio::main] async fn main() -> Result<()> { let client = reqwest::ClientBuilder::new() .user_agent("Intro to Rust!") .build()?; let response: JsonResponse = client .get("https://httpbin.org/json") .send() .await? .json() .await?; println!("{response:#?}"); Ok(()) } #[derive(serde::Deserialize, Debug)] struct JsonResponse { slideshow: Slideshow, } #[derive(serde::Deserialize, Debug)] struct Slideshow { author: String, date: String, title: String, } ``` --- ## Web server * Many different server libraries * I'm a fan of axum, built on hyper (same as reqwest) * Ties in really nicely with the tokio ecosystem ```rust use std::net::SocketAddr; use anyhow::Result; use axum::{extract::State, routing::get, Json, Router}; use clap::Parser; use rand::Rng; use reqwest::Client; async fn hello_world() -> &'static str { "Hello World!" } #[derive(serde::Serialize)] struct RandomNumber { number: u64, } async fn random_number() -> Json<RandomNumber> { Json(RandomNumber { number: rand::thread_rng().gen(), }) } async fn relay(State(client): State<Client>) -> Json<serde_json::Value> { let res = async { client .get("https://httpbin.org/json") .send() .await? .json() .await } .await; Json(match res { Ok(value) => value, Err(e) => serde_json::to_value(ErrorOccurred { error: e.to_string(), }) .unwrap(), }) } #[derive(serde::Serialize)] struct ErrorOccurred { error: String, } #[derive(clap::Parser)] struct Opt { #[clap(long, default_value = "0.0.0.0:3000")] bind: SocketAddr, } #[tokio::main] async fn main() -> Result<()> { let Opt { bind } = Opt::parse(); let client = reqwest::ClientBuilder::new() .user_agent("axum + reqwest") .build()?; let app = Router::new() .route("/hello-world", get(hello_world)) .route("/random-number", get(random_number)) .route("/relay", get(relay)) .with_state(client); axum::Server::bind(&bind) .serve(app.into_make_service()) .await?; Ok(()) } ``` --- ## FP comparison (Especially versus Haskell) * FP-style * Traits * ADTs * Immutable * Strong typing * Iterators * Expression oriented * Unlike FP * No garbage collection * Avoids more advanced techniques like Higher Kinded Types * Prefers specific notation (like `?` for errors) versus general notation (like `do`-notation) --- ## Other Rust use cases * Rust is great for application-level development * Lack of runtime and high performance give other opportunities * Embedded * Realtime * Write an entire operating system * Blockchain smart contracts (Cosmos, Solana, Near) * Web frontends (via WASM) * Serverless/AWS Lambda/Cloudflare Workers --- ## Personal takeaways * FP is a fundamentally great paradigm * Rust takes the best of FP * Wonderful tooling, great set of libraries * Manual memory management is a burden most apps don't need * But it gets much easier over time * And the other benefits of the language balance it out * I highly recommend learning Rust and considering it for future projects --- ## Questions? * If we have time, happy to demo some code * Thanks for listening!