I am a doctoral researcher in scientific machine learning at Chair of scientific computing (Chair 05 at TUM School of CIT).
"Idiomatic coding means following the conventions of a given language. It is the most concise,
convenient, and common way of accomplishing a task in that language, rather than forcing it
to work in a way the author is familiar with from a different language."
— Tim Mansfield
if (input !== null && input !== undefined && input !== "") {
console.log("Input is valid");
}
if (input) {
console.log("Input is valid");
}
fn greet(user: String) {
println!("Hello, {}!", user); // `user` goes out of scope
}
fn farewell(user: String) {
println!("Bye {}!", user);
}
fn main() {
let user = String::from("Marty Friedman");
greet(user); // Passes ownership to greet
farewell(user); // Error: Use of moved value `user`
}
fn greet(user: &String) {
println!("Hello, {}!", user);
}
fn farewell(user: &String) {
println!("Bye {}!", user);
}
fn main() {
let user = String::from("Marty Friedman");
greet(&user); // Immutable borrow
farewell(&user); // Immutable borrow
}
fn main() {
let user: &str = "Guthrie Govan";
greet(&user); // Error: Expected reference `&String`
farewell(&user);
}
Solution: Prefer borrowed types over borrowing owned type for function arguments to support both.
fn greet(user: &str) {
println!("Hello, {}!", user);
}
fn farewell(user: &str) {
println!("Bye {}!", user);
}
fn main() {
let user: &str = "Guthrie Govan";
greet(user); farewell(user);
// This works too!
let user = String::from("Marty Friedman");
// Note: Use `.as_ref()` to explicitly convert to `&str`
// Here: Compiler figures it out!
greet(&user); farewell(&user);
}
fn print_slice(slice: &[i32]) {
println!("Slice: {:?}", slice);
}
fn main() {
let arr: [i32; 4] = [1, 2, 3, 4];
print_slice(&arr);
// This works too!
let vec: Vec<i32> = vec![5, 6, 7, 8];
// Again, compiler figures out `&Vec<i32>` -> `&[i32]`
print_slice(&vec);
}
let data: Vec<i32> = vec![0, 1, 2, 3, 4];
let slice: &[i32] = &data[1..4]; // &[1,2,3]
let slice: &[i32] = &data[1..=4]; // &[1,2,3,4]
let data = {
let mut data: Vec<i32> = vec![1, 2, 3];
data.push(4); // Requires mutation
data
};
// Here `data` is immutable
let sum: i32 = data.iter().sum(); // sum is 10
let s = String::from("hello");
println!("Hello {}", &s); // Borrowing avoids cloning
format!let name: &str = "Jason Becker";
let mut hello_name = String::from("Hello ");
hello_name.push_str(name);
hello_name.push_str('!');
// Better/easier with `format!`
let hello_name: String = format!("Hello {}!", name);
enum Direction {
North, South,
East, West,
}
fn main() {
let dir = Direction::North;
match dir {
Direction::North => println!("Going North"),
_ => println!("Not going North")
}
}
enum Color {
RGB(u8, u8, u8),
RGBA(u8, u8, u8, u8),
}
fn main() {
let red = Color::RGB(255, 0, 0);
// Error! Use tuple structs or pattern matching!
println!("Color: RGB({red.0}, {red.1}, {red.2})");
}
enum Color {
RGB(u8, u8, u8),
RGBA(u8, u8, u8, u8),
}
fn main() {
let red = Color::RGB(255, 0, 0);
match red {
Color::RGB(r, g, b) => println!(...),
Color::RGBA(r, g, b, a) => println!(...),
}
// To only match `Color::RGB`
if let Color::RGB(r, g, b) = red {
println!("Color: RGB({r}, {g}, {b})");
}
}
enum Color {
Srgba(Srgba),
LinearRgba(LinearRgba),
// [...]
}
struct Srgba {
red: f32,
green: f32,
blue: f32,
alpha: f32,
}
enum AccountState {
Active { user: String, balance: u32 },
Suspended { user: String },
}
Active variant to Suspended variant if balance is zero?
fn deactivate(accs: &mut AccountState) {
if let AccountState::Active { user, balance: 0 } = accs {
// Note: We do not own `user`
*accs = AccountState::Suspended {
user: std::mem::take(user)
};
}
}
// Tuple struct
struct Meters(f64); // The newtype pattern
// Type alias
type Meters = f64; // Does not create a custom type
type:type creates an alias but doesn't enforce type safety (type Meters = f64; is still considered as f64)struct Point { x: f64, y: f64, }
struct Rectangle {
top_left: Point,
bottom_right: Point,
}
enum Message {
Quit,
Shift { x: f64, y: f64 },
ChangeColor(Color),
Write(String),
// [...]
}
impl Rectangle {
fn shift(&mut self, dx: f64, dy: f64) {
self.top_left.x += dx;
self.top_left.y += dy;
self.bottom_right.x += dx;
self.bottom_right.y += dy;
}
}
fn main() {
let top_left = Point { x: 0., y: 1. };
let bottom_right = Point { x: 1., y: 0. };
let mut rect = Rectangle { top_left, bottom_right };
let msg = Message::Shift { x: 1., y: 1. };
if let Message::Shift { x: dx, y: dy } = msg {
rect.shift(dx, dy);
}
}
newimpl Rectangle {
fn new(top_left: Point, bottom_right: Point) -> Self {
Self { top_left, bottom_right }
}
}
fn main() {
let top_left = Point { x: 0., y: 1. };
let bottom_right = Point { x: 1., y: 0. };
let rect = Rectangle::new(top_left, bottom_right);
}
trait Area {
fn area(&self) -> f64;
}
impl Area for Rectangle {
fn area(&self) -> f64 {
let width = self.bottom_right.x - self.top_left.x;
let height = self.top_left.y - self.bottom_right.y;
width * height
}
}
// Other structs can implement this trait
#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
struct Point { x: f64, y: f64 }
{:?}.Point::default()..clone().== and != for comparisons.NaN != NaN)PartialEq by adding reflexivity (x == x for all x).HashMap keys>, >=, < and <=.f32)PartialOrd by requiring that all pairs are comparable.x < y, x > y, or x == y is true for all x and yDefault traitimpl Default for Rectangle {
fn default() -> Self {
Self {
top_left: Point { x: 0., y: 1. },
bottom_right: Point { x: 1., y: 0. }
}
}
}
Default traitfn main() {
let rect = Rectangle {
bottom_right: Point { x: 2., y: 0. },
..Default::default()
};
}
dynlet readable: &mut dyn std::io::Read = if arg == "-" {
&mut std::io::stdin() // of `Stdin` type
} else {
&mut std::fs::File::open(arg)? // of `File` type
};
let mut buffer = String::new();
readable.read_to_string(&mut buffer)?;
readable is resolved during runtime.read implementation to invoke
enum Shape {
Circle { center: Point, radius: f64 },
Rectangle { top_left: Point, bottom_right: Point }
}
impl Area for Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle { center, radius } => {
std::f64::consts::PI * radius * radius
},
Shape::Rectangle { top_left, bottom_right } => {
let width = bottom_right.x - top_left.x;
let height = top_left.y - bottom_right.y;
width * height
}
}
}
}
struct Rectangle { top_left: Point, bottom_right: Point, }
struct Circle { center: Point, radius: f64, }
impl Area for Rectangle {
fn area(&self) -> f64 {
let width = self.bottom_right.x - self.top_left.x;
let height = self.top_left.y - self.bottom_right.y;
width * height
}
}
impl Area for Circle {
fn area(&self) -> f64 {
f64::consts::PI * self.radius * self.radius
}
}
let shapes: Vec<&dyn Area> = vec![&circle, &rect];
let name = String::from("ata"); // From<&str>
let name_slice: &str = name.as_ref(); // AsRef<&str>
let name_bytes: &[u8] = name.as_ref(); // AsRef<&[u8]>
println!("{}", name_slice); // ata
println!("{:?}", name_bytes); // [97, 116, 97]
From<T>: Converts from type `T` to the target type `Self`.TryFrom<T>: Converts from type `T` to the target type `Self`, but may fail.AsRef<T>: Provides a cheap, reference-to-reference conversion from `&Self` to `&T`.AsMut<T>: Provides a cheap, mutable reference-to-reference conversion from `&mut Self` to `&mut T`.From<T> for U implies Into<U> for T ⇒ you should prefer implementing the From traitFrom<u16> is implemented for u32 because a smaller integer can always be converted to a bigger integer.From<u32> is NOT implemented for u16 because the conversion may not be possible if the integer is too big.TryFrom<u32> is implemented for u16 and returns an error if the integer is too big to fit in.Debug trait provides printing via {:?}#[derive(Debug)]
struct Point { x: f64, y: f64 }
fn main() {
let p = Point { x: 0., y: 1. };
println!("{:?}", p); // Point { x: 0.0, y: 1.0 }
println!("{}", p); // Error: Implement `Display` trait
}
Display traitDisplay for Pointimpl std::fmt::Display for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>)
-> std::fmt::Result {
write!(f, "({},{})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 0., y: 1. };
println!("{:?}", p); // Point { x: 0.0, y: 1.0 }
println!("{}", p); // (0,1)
}
derive_more crate to workaround boilerplate code.trait Area {
fn area(&self) -> f64 {
0.
}
}
struct Point { x: f64, y: f64, }
impl Area for Point {} // Uses the default implementation
Rectangle and Circle structs).fn print_area<T: Area>(shape: &T) {
let area = shape.area();
println!("Area: {}", area);
}
fn print_area(shape: &impl Area) {
let area = shape.area();
println!("Area: {}", area);
}
where clausesfn display_and_print_areas<T, U>(shape1: &T, shape2: &U)
where
T: Area + std::fmt::Display,
U: Area + std::fmt::Display,
{
let area1 = shape1.area();
let area2 = shape2.area();
println!("Shape {} has area {}", shape1, area1);
println!("Shape {} has area {}", shape2, area2);
}
undefined, null, exception...enum Option<T> {
None, // no value is available
Some(T), // value is available
}
enum Result<T, E> {
Ok(T), // operation is successful
Err(E), // operation returned an error
}
Option<T>fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("Alice"))
} else {
None
}
}
Option<T>None:if let Some(user) = find_user(id) { ... }
None: let user = find_user(id).unwrap();
// Prefer `.expect` over `.unwrap`
let user = find_user(id).expect("User must exist");
None: let user = find_user(id)
.unwrap_or(String::from("Guest"));
fn foo(a: Option<i32>, b: Option<i32>) {
if let Some(a) = a {
println!("a {}", a);
}
if let Some(b) = b {
println!("b {}", b);
}
}
fn main() {
foo(None, None);
foo(Some(1), None); // Some(_) is required
}
Some? It may too verbose for the user.fn foo2<T, U>(a: T, b: U)
where
T: Into<Option<i32>>,
U: Into<Option<i32>>,
{
if let Some(a) = a.into() {
println!("a {}", a);
}
if let Some(b) = b.into() {
println!("b {}", b);
}
}
fn main() {
foo(None, None);
foo(1, None); // Some(_) is not explicitly required
}
Error<T, E>fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0. {
Err(String::from("Cannot divide by zero"))
} else {
Ok(a / b)
}
}
Error<T, E> (again using utility functions)Err:if let Ok(val) = divide(a,b) { ... }
Err: let val = divide(a,b).unwrap();
// Prefer `.expect` over `.unwrap`
let val = divide(a,b).expect("Division must succeed");
Err: // Same as `.unwrap_or(0.)` for f64
let val = divide(a,b).unwrap_or_default();
E in Result<T, E> you can propagate the errorfn read_user(path: &str) -> Result<String, std::io::Error> {
let f = std::fs::File::open(path);
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut buffer = String::new();
match f.read_to_string(&mut buffer) {
Ok(_) => Ok(buffer),
Err(e) => Err(e)
}
}
? operator to propagate the errorfn read_user(path: &str) -> Result<String, std::io::Error> {
let mut f = std::fs::File::open(path)?;
let mut buffer = String::new();
f.read_to_string(&mut buffer)?;
Ok(buffer)
}
FromStr for Point structimpl std::str::FromStr for Point {
type Err = std::num::ParseFloatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let coords: Vec<&str> = s
.trim_matches(|p| p == '(' || p == ')')
.split(',')
.collect();
let x: f64 = coords[0].parse()?;
let y: f64 = coords[1].parse()?;
Ok(Self { x, y })
}
}
FromStr is often used through str's parse methodparse returns Result<_, std::num::ParseIntError>fn read_number(path: &str) -> Result<i32, std::io::Error> {
let mut f = std::fs::File::open(path)?;
let mut buffer = String::new();
f.read_to_string(&mut buffer)?;
let parsed_num = buffer.trim().parse::<i32>()?; // Error
Ok(parsed_num)
}
Err types to StringErr types to Stringfn read_number(path: &str) -> Result<i32, String> {
let mut f = std::fs::File::open(path)
.map_err(|e| format!("Failed to open file: {}", e))?;
let mut buffer = String::new();
f.read_to_string(&mut buffer)
.map_err(|e| format!("Failed to read file: {}", e))?;
let parsed_num = buffer.trim().parse::<i32>()
.map_err(|e| format!("Failed to parse: {}", e))?;
Ok(parsed_num)
}
#[derive(Debug)]
enum FileReadError {
FileOpen(std::io::Error),
Parse(std::num::ParseIntError),
}
// To be compatible with the ecosystem
impl std::error::Error for FileReadError { }
std::error::Error trait makes your error type idiomatic and compatible with Rust's standard library.Displayimpl std::fmt::Display for FileReadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>)
-> std::fmt::Result {
write!(f, "{:?}", self)
}
}
From<T> for all the wrapped error typesimpl From<std::io::Error> for FileReadError {
fn from(e: std::io::Error) -> Self {
FileReadError::FileOpen(e)
}
}
impl From<std::num::ParseIntError> for FileReadError {
fn from(e: std::num::ParseIntError) -> Self {
FileReadError::Parse(e)
}
}
fn read_number(path: &str) -> Result<i32, FileReadError> {
let mut f = std::fs::File::open(path)?;
let mut buffer = String::new();
f.read_to_string(&mut buffer)?;
let parsed_num = buffer.trim().parse()?;
Ok(parsed_num)
}
thiserror to skip the headachethiserror provides you useful macros#[derive(Debug, thiserror::Error)]
enum FileReadError {
#[error("Failed to open file: {0}")]
FileError(#[from] std::io::Error),
#[error("Failed to parse number: {0}")]
ParseError(#[from] std::num::ParseIntError),
}
thiserror is transparent: It behaves like a handwritten error implementation.for loop in Rustfor x in 0..4 { // x in {0, 1, 2, 3}
println!("{}", x);
}
0..4 are 'iterators'. Iterators provide the .next methodlet mut range = 0..4;
loop {
match range.next() {
Some(x) => println!("{}", x),
None => break,
}
}
Iterator traitIterator: core trait with the method .next()IntoIterator: Enables conversion into an iteratorExactSizeIterator: Additional trait to provide .len() informationDoubleEndedIterator: Enables reverse iteration with .next_back().iter(), which iterates over &T.iter_mut(), which iterates over &mut T.into_iter(), which iterates over Tstruct Counter { count: usize }
impl Iterator for Counter {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
Some(self.count)
}
}
let counter = Counter { count: 0 };
let mut it = counter.into_iter();
it.next(); // Some(1)
let counter = Counter { count: 0 };
// Consumes `counter` and calls .next() till None is returned
for c in counter { ... }
for looplet nums = vec![1, 2, 3];
for idx in 0..nums.len() {
println!("{}", nums[idx]); // unnecessary bounds check
}
for num in &nums {
println!("{}", *num); // no bounds check but still safe
}
.enumerate if you need the indexfor (idx, num) in nums.iter().enumerate() {
println!("idx {}: {}", idx, num);
}
let one_to_hundred: Vec<u8> = (1..=100).collect();
let nums: Vec<i32> = vec![1, 2, 3, 4];
let even: Option<&i32> = nums
.iter()
.find(|&x| x % 2 == 0); // 2
let sum = nums
.iter()
.fold(0, |acc, &x| acc + x); // 15
reduce, product, min, max, any, all...let squares: Vec<i32> = nums.iter().map(|&x| x*x).collect();
let first_three: Vec<&i32> = nums.iter().take(3).collect();
let evens: Vec<&i32> = nums
.iter()
.filter(|&x| x % 2 == 0)
.collect();
take, skip, peekable...let add: &dyn Fn(&i32) -> i32 = &|&x| x + outer_var;
Fn requires immutable borrow of the environment and can be called multiple timesFnMut requires mutable borrow of the environment and can be called multiple times (sequentially)FnOnce requires ownership of the environment and can only be called once because they consume their environment.let a = 1;
nums
.iter()
.for_each(|&x| println!("{}", x + a));
// impl Fn
let a = 1;
let add_a = |x: i32| x + a; // add_a(x)
// FnMut
let mut a: u32 = 1;
let mut inc_a = || a += 1; // inc_a()
// FnOnce
let name = String::from("Aylin");
let say_hello = || {
let s = name;
println!("Hello, {}", s);
}; // say_hello();
move keywordlet data = vec![1, 2, 3];
let closure = move || println!("captured {data:?} by value");
// Error
println!("{:?}", data); // data is not available,
// it is owned by the closure
move closures may still implement Fn or FnMut
fn apply_closure<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(x)
}
let double = |x| x * 2;
println!("{}", apply_closure(double, 5)); // 10
fn create_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
let multiply_by_3 = create_multiplier(3);
println!("{}", multiply_by_3(5)); // 15
move is used because the closure outlives the function's scopeDebug and Clone.Option and Result, propagate them with ?, and simplify with custom error types or libraries like thiserror.