knowledge/technology/dev/programming/languages/Rust.md
2024-03-06 13:15:41 +01:00

25 KiB
Raw Blame History

website source mime extension obj
https://www.rust-lang.org https://doc.rust-lang.org/book text/rust rs concept

Rust

Rust is a statically-typed programming language known for its emphasis on performance, safety, and concurrency. Originally developed by Mozilla, Rust has gained popularity for its ability to provide low-level control over system resources without sacrificing memory safety. Rust uses Cargo as its package manager and build tool.

Syntax

Your application starts within the main function, so the simplest application is this:

fn main() {

}

Variables

You can declare variables. Variables are immutable by default, if you need to change them you have to use the mut keyword. Every variable is strongly typed, but you can either ommit type information and let the compiler infer the type or explicitly state it. Constants which never change can be made as well.

let var = "Hello";
let mut mutable = "World";
let explicit_num: isize = 0;
const NINE_K: isize = 9000;

Data Types & Ownership

Every variable in Rust is strongly typed. You can define your own types and use the compiler together with an algebraic type system to your advantage.

In Rust, primitive types are classified into two categories: scalar types and compound types:

Scalar Types

  1. Integers:
    • i8: Signed 8-bit integer
    • i16: Signed 16-bit integer
    • i32: Signed 32-bit integer
    • i64: Signed 64-bit integer
    • i128: Signed 128-bit integer
    • u8: Unsigned 8-bit integer
    • u16: Unsigned 16-bit integer
    • u32: Unsigned 32-bit integer
    • u64: Unsigned 64-bit integer
    • u128: Unsigned 128-bit integer
    • isize: Platform-dependent signed integer
    • usize: Platform-dependent unsigned integer
  2. Floating-point:
    • f32: 32-bit floating-point number
    • f64: 64-bit floating-point number
  3. Characters:
    • char: A Unicode character (4 bytes)
  4. Booleans:
    • bool: Boolean type representing either true or false

Compound Types

  1. Arrays:
    • [T; N]: Fixed-size array of elements of type T and length N
  2. Tuples:
    • (T1, T2, ..., Tn): Heterogeneous collection of elements of different types

Pointer Types

  1. References:
    • &T: Immutable reference
    • &mut T: Mutable reference
  2. Raw Pointers:
    • *const T: Raw immutable pointer
    • *mut T: Raw mutable pointer

Rust enforces some rules on variables in order to be memory safe. So there are three kinds of variables you could have:

  • Owned Variable T: You are the owner of this variable with data type T
  • Reference &T: You have a read only reference of the variables content
  • Mutable Reference &mut T: You have a modifiable reference to the variable

Note: If a function does not need to mutate or own a variable, consider using a reference

Conditionals

Conditionals like if and match can be used, while match can do more powerful pattern matching than if.

let age = 20;
if age > 18 {
    println!("Adult");
} else if age == 18 {
    println!("Exactly 18");
} else {
    println!("Minor");
}

match age {
    18 => println!("Exactly 18"),
    _ => println!("Everything else")
}

Loops

There are three types of loops.

loop {
    println!("Going on until time ends");
}

for item in list {
    println!("This is {item}");
}

while condition {
    println!("While loop");
}

You can break out of a loop or continue to the next iteration:

for i in 0..10 {
    if i % 2 == 0 {
        continue
    }
    if i % 5 == 0 {
        break;
    }
    println!("{i}");
}

If you have nested loops you can label them and use break and continue on specific loops:

'outer_loop: for i in 0..10 {
    'inner_loop: for x in 0..10 {
        if (x*i) > 40 {
            continue 'outer_loop;
        }
        print("{x} {i}");
    }
}

Functions

One can use functions with optional arguments and return types. If you return on the last line, you can ommit the return keyword and ; to return the value.

fn printHello() {
    println!("Hello");
}

fn greet(name: &str) {
    println!("Hello {name}");
}

fn add_two(a: isize) -> isize {
    a + 2 // same as "return a + 2;"
}

The default return type of a function is the Unit Type (). You can also signal that a function will never return with !

fn infinity() -> ! {
    loop {}
}

Enums

Rust has enums which can even hold some data.

enum StatusCode {
    NOT_FOUND,
    OK,
    Err(String)
}

let err_response = StatusCode::Err(String::from("Internal Error"));

match err_response {
    StatusCode::Ok => println!("Everything is fine"),
    StatusCode::NOT_FOUND => println!("Not found");
    StatusCode::Err(err) => println!("Some error: {err}"); // will print "Some error: Internal Error"
}

// other way to pattern match for a single pattern
if let StatusCode::Err(msg) = err_response {
    println!("{msg}!");
}

Structs

Rust has some object-oriented features like structs.

struct Person {
    first_name: String,
    age: isize
}

impl Person {
    fn new(first_name: &str) -> Self {
        Self {
            first_name: first_name.to_string(),
            age: 0
        }
    }

    fn greet(&self) {
        println!("Hello {}", self.first_name);
    }

	fn get_older(&mut self) {
		self.age += 1;
	}
}

Comments

Rust has support for comments. There are regular comments, multiline comments and documentation comments. Documentation comments get used when generating documation via cargo doc and inside the IDE.

// This is a comment

/*
This
is multiline
comment
*/

/// This function does something.
/// Documentation comments can be styled with markdown as well.
/// 
/// # Example
///
/// If you place a rust code block here, you can provide examples on how to use this function and `cargo test` will automatically run this code block when testing.
fn do_something() {

}

Modules

You can split your code up into multiple modules for better organization.

// will search for `mymod.rs` or `mymod/mod.rs` beside the source file and include it as `mymod`;
mod mymod;

// inline module
mod processor {
    struct AMD {
        name: String
    }
}

fn main() {
    // full syntax
    let proc = processor::AMD{ name: "Ryzen".to_string() };

    // you can put often used symbols in scope
    use processor::AMD;
    let proc = AMD{ name: "Ryzen".to_string() };
}

Generics

Generics let you write code for multiple data types without repeating yourself. Instead of an explicit data type, you write your code around a generic type.

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

// you can declare methods which only work for specific data types.
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

impl<X1, Y1> Point<X1, Y1> {
    // define generic types on functions
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

enum Option<T> {
    Some(T),
    None,
}

// you can have multiple generic types
enum Result<T, E> {
    Ok(T),
    Err(E),
}

Traits

Traits let you define shared behavior on structs. You define what methods a struct should have and it is up the the struct to implement them.

pub trait Summary {
    fn summarize(&self) -> String;
}

// you can define default behaviour
pub trait DefaultSummary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

// use traits as parameters
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

// you can also restrict generic types to only ones that implement certain traits
pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

// even multiple at once
pub fn notify<T: Summary + Display>(item: &T) {}

// another way to restrict types to traits
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{}

// you can restrict at impl level too
struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    // this function is only available if T has Display and PartialOrd
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

Common traits you can implement on your types are:

  • Clone: Allows creating a duplicate of an object.
pub trait Clone {
    fn clone(&self) -> Self;
}
  • Copy: Types that can be copied by simple bitwise copying.
pub trait Copy: Clone {}
  • Debug: Enables formatting a value for debugging purposes.
pub trait Debug {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}
  • Default: Provides a default value for a type.
pub trait Default {
    fn default() -> Self;
}
  • Eq and PartialEq: Enables equality comparisons.
pub trait Eq: PartialEq<Self> {}

pub trait PartialEq<Rhs: ?Sized = Self> {
    fn eq(&self, other: &Rhs) -> bool;
}
  • Ord and PartialOrd: Enables ordering comparisons.
pub trait Ord: Eq + PartialOrd<Self> {
    fn cmp(&self, other: &Self) -> Ordering;
}

pub trait PartialOrd<Rhs: ?Sized = Self>: PartialEq<Rhs> {
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
}
  • Iterator: Represents a sequence of values.
pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // Other iterator methods...
}
  • Read and Write: For reading from and writing to a byte stream.
pub trait Read {
    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error>;
}

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
}
  • Fn, FnMut, and FnOnce: Traits for function types with different levels of mutability.
pub trait Fn<Args> {
    // function signature
}

pub trait FnMut<Args>: Fn<Args> {
    // function signature
}

pub trait FnOnce<Args>: FnMut<Args> {
    // function signature
}
  • Drop: Specifies what happens when a value goes out of scope.
pub trait Drop {
    fn drop(&mut self);
}
  • Deref and DerefMut: Used for overloading dereference operators.
pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

pub trait DerefMut: Deref {
    fn deref_mut(&mut self) -> &mut Self::Target;
}
  • AsRef and AsMut: Allows types to be used as references.
pub trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

pub trait AsMut<T: ?Sized>: AsRef<T> {
    fn as_mut(&mut self) -> &mut T;
}
  • Index and IndexMut: Enables indexing into a data structure.
pub trait Index<Idx: ?Sized> {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

pub trait IndexMut<Idx: ?Sized>: Index<Idx> {
    fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}
  • Send and Sync: Indicate whether a type is safe to be transferred between threads (Send) or shared between threads (Sync).
unsafe trait Send {}
unsafe trait Sync {}
  • Into and From: Facilitates conversions between types.
pub trait Into<T> {
    fn into(self) -> T;
}

pub trait From<T> {
    fn from(T) -> Self;
}
  • Display: Formatting values for user display.
pub trait Display {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}
  • FromStr and ToString: Enables conversion between strings and other types.
pub trait FromStr {
    type Err;
    fn from_str(s: &str) -> Result<Self, Self::Err>;
}

pub trait ToString {
    fn to_string(&self) -> String;
}
  • Error: Represents errors that can occur during the execution of a program.
pub trait Error: Debug {
    fn source(&self) -> Option<&(dyn Error + 'static)>;
}
  • Add, Sub, Mul, and Div: Traits for arithmetic operations.
pub trait Add<RHS = Self> {
    type Output;
    fn add(self, rhs: RHS) -> Self::Output;
}

// Similar traits for Sub, Mul, and Div

Closures

Closures are anonymous functions. They can be assigned to variables, passed as parameters or returned from a function.

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

// this function takes ownership over variables it uses as noted by `move`
thread::spawn(
    move || println!("From thread: {:?}", list)
    ).join().unwrap();

// To use closures as parameters, specify the type. It is one of the Fn traits
impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

Iterators

The iterator pattern allows you to perform some task on a sequence of items in turn. An iterator is responsible for the logic of iterating over each item and determining when the sequence has finished. When you use iterators, you dont have to reimplement that logic yourself.

let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

With this you can do functional programming with the iterators.

Some functions on them include:

  • chain(other): An iterator that links two iterators together, in a chain.
  • cloned(): An iterator that clones the elements of an underlying iterator.
  • copied(): An iterator that copies the elements of an underlying iterator.
  • cycle(): An iterator that repeats endlessly.
  • empty(): An iterator that yields nothing.
  • enumerate(): An iterator that yields the current count and the element during iteration.
  • filter(predicate): An iterator that filters the elements of iter with predicate.
  • filterMap(f): An iterator that uses f to both filter and map elements from iter.
  • flat_map(f): An iterator that maps each element to an iterator, and yields the elements of the produced iterators.
  • flatten(): An iterator that flattens one level of nesting in an iterator of things that can be turned into iterators.
  • from_fn(f): An iterator where each iteration calls the provided closure F: FnMut() -> Option<T>.
  • fuse(): An iterator that yields None forever after the underlying iterator yields None once.
  • inspect(f): An iterator that calls a function with a reference to each element before yielding it.
  • map(f): An iterator that maps the values of iter with f.
  • map_while(f): An iterator that only accepts elements while predicate returns Some(_).
  • once(value): An iterator that yields an element exactly once.
  • peekable(): An iterator with a peek() that returns an optional reference to the next element.
  • repeat(value): An iterator that repeats an element endlessly.
  • rev(): A double-ended iterator with the direction inverted.
  • skip(n): An iterator that skips over n elements of iter.
  • skip_while(f): An iterator that rejects elements while predicate returns true.
  • step_by(n): An iterator for stepping iterators by a custom amount.
  • successors(first, f): A new iterator where each successive item is computed based on the preceding one.
  • take(n): An iterator that only iterates over the first n iterations of iter.
  • take_while(f): An iterator that only accepts elements while predicate returns true.
  • zip(a, b): An iterator that iterates two other iterators simultaneously.

Standard Library

Rust, a systems programming language known for its focus on safety and performance, comes with a rich standard library that provides a wide range of modules to handle common tasks.

  1. std::collections: Data structures like Vec, HashMap, etc.
  2. std::fs: File system manipulation and I/O operations.
  3. std::thread: Facilities for concurrent programming with threads.
  4. std::time: Time-related types and functionality.
  5. std::io: Input and output facilities.
  6. std::path: Path manipulation utilities.
  7. std::env: Interface to the environment, command-line arguments, etc.
  8. std::string: String manipulation utilities.
  9. std::cmp: Comparison traits and functions.
  10. std::fmt: Formatting traits and utilities.
  11. std::result: Result type and related functions.
  12. std::error: Error handling utilities.
  13. std::sync: Synchronization primitives.
  14. std::net: Networking functionality.
  15. std::num: Numeric types and operations.
  16. std::char: Character manipulation utilities.
  17. std::mem: Memory manipulation utilities.
  18. std::slice: Slice manipulation operations.
  19. std::marker: Marker traits for influencing Rust's type system.

Macros

Weve used macros like println! before, but we havent fully explored what a macro is and how it works. The term macro refers to a family of features in Rust: declarative macros with macro_rules! and three kinds of procedural macros:

  • Custom #[derive] macros that specify code added with the derive attribute used on structs and enums
  • Attribute-like macros that define custom attributes usable on any item
  • Function-like macros that look like function calls but operate on the tokens specified as their argument

Fundamentally, macros are a way of writing code that writes other code, which is known as metaprogramming. All of these macros expand to produce more code than the code youve written manually.

Declarative macros work almost like a match statement.

macro_rules! mymacro {
	($expression:expr) => {
		println!("{}", $expression)
	};
	($expression:expr, $other:expr) => {
		println!("{} {}", $expression, $other)
	};
}

mymacro!("Hello World");
mymacro!("Hello", "World");

This macro gets expanded to the code inside the macro_rules! section with the provided arguments. For more information on macros, see the docs.

unsafe Rust

Rust is focused strongly on safety, but sometimes doing something dangerous is necessary. In this case you can use the unsafe keyword. unsafe should be used only when needed as it may cause undefinied behaviour, but when debugging you can solely focus on your unsafe blocks as all potential dangerous operations are neatly packaged in them.

There are two types of using unsafe:

  • unsafe blocks lets you call dangerous code. With this you can wrap unsafe code in a safe function with checks to call.
fn write_to_serial(data: &[u8]) {
    assert!(data.is_valid());

    unsafe {
        // doing potentially unsafe things
        write_to_serial_unchecked(data);
    }
}
  • unsafe functions can only be called from unsafe blocks.
unsafe fn write_to_serial_unchecked(data: &[u8]) {
    // unsafe operation
}

Crates

  • anyhow: Flexible concrete Error type built on std::error::Error
  • itertools: Extra iterator adaptors, iterator methods, free functions, and macros
  • num_enum: Procedural macros to make inter-operation between primitives and enums easier

Encoding

  • bincode: A binary serialization / deserialization strategy for transforming structs into bytes and vice versa!
  • serde: A generic serialization/deserialization framework
  • serde_json: A JSON serialization file format
  • serde_yaml: YAML data format for Serde
  • bson: Encoding and decoding support for BSON in Rust
  • hex: Encoding and decoding data into/from hexadecimal representation
  • toml: A native Rust encoder and decoder of TOML-formatted files and streams.
  • base64: encodes and decodes base64 as bytes or utf8

Algorithms

  • rand: Random number generators and other randomness functionality

Debugging

  • log: A lightweight logging facade for Rust
  • env_logger: A logging implementation for log which is configured via an environment variable

Mail

Visualization

  • plotters: A Rust drawing library focus on data plotting for both WASM and native applications
  • plotly: A plotting library powered by Plotly.js
  • textplot: Terminal plotting library

Templates

Media

  • image: Imaging library. Provides basic image processing and encoders/decoders for common image formats.

CLI

  • rustyline: Rustyline, a readline implementation based on Antirez's Linenoise
  • clap: A simple to use, efficient, and full-featured Command Line Argument Parser
  • crossterm: A crossplatform terminal library for manipulating terminals
  • indicatif: A progress bar and cli reporting library for Rust
  • argh: Derive-based argument parser optimized for code size
  • owo-colors: Zero-allocation terminal colors that'll make people go owo
  • yansi: A dead simple ANSI terminal color painting library

Compression

  • flate2: DEFLATE compression and decompression exposed as Read/BufRead/Write streams. Supports miniz_oxide and multiple zlib implementations. Supports zlib, gzip, and raw deflate streams.
  • tar: A Rust implementation of a TAR file reader and writer.
  • zstd: Binding for the zstd compression library
  • unrar: list and extract RAR archives

Databases

  • rusqlite: Ergonomic wrapper for SQLite
  • sqlx: The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, and SQLite.
  • mongodb: The official MongoDB driver for Rust

Data and Time

  • chrono: Date and time library for Rust
  • humantime: A parser and formatter for std::time::{Duration, SystemTime}

HTTP

  • hyper: A fast and correct HTTP library
  • reqwest: higher level HTTP client library
  • actix-web: Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust

Text

  • regex: An implementation of regular expressions for Rust. This implementation uses finite automata and guarantees linear time matching on all inputs.
  • comfy-table: An easy to use library for building beautiful tables with automatic content wrapping
  • similar: A diff library for Rust

Concurrency

  • parking_lot: More compact and efficient implementations of the standard synchronization primitives
  • crossbeam: Tools for concurrent programming
  • rayon: Simple work-stealing parallelism for Rust

Async

  • tokio: An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications
  • futures: An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces