Exercises while going through official book from doc.rust-lang.org
Notes taken from following along The Rust Book
- Rustlings - Small exercises to solve in Rust
Shopify Repositories Using Rust In Production:
-
rustup
is a CLI tool for managing Rust versionsrustc --version
to check if rust is installed correctlyrustup doc
to get documentation in browser
-
Cargo is the Rust build system and package manager
$ cargo new
to create a new project- "crates" => Packages of code
- TOML is cargo's configuration format
- Source files for codes live in the
/src
dir $ cargo check
to only compile without creating an executable$ cargo run
to execute (and re-compile if files have changed)$ cargo build
to compile and create an executable$ cargo build --release
to compile with optimizations => intarget/release
$ cargo doc --open
to open the documentation for the code/libraries in the project in a browser (very smart!)- "binary crates" are executable, "library crates" can't, are intended to be used in other programs
-
Compiling and running are separate steps
rustc
runs the rust compiler => outputs a binary executable (prefer$ cargo build
for more complex projects)- Rust is "ahead of time compiled" meaning, a binary executable can be run by a machine without rust installed
rustfmt
is a formatting tool- indent with four spaces
- snake_case as conventional style for function and variable names
- It is common to shadow existing variable names when converting type to reuse the variable name
let mut guess = String::new();
// ...
let guess: u32 = guess.trim().parse().expect("Please type a number!"); // note this new var is immutable
// `parse` method on strings converts it to another type
// the colon `:` after the var name tells Rust that the type will be annotated (here an unsigned 32-bit integer)
main
function is special, it's always the first code that runs in every executable rust program.- Function body is wrapped in
{}
for all function bodies println!
calls a rust macro.println
without the!
is a normal function.- Macros have some different rules than functions
;
indicates the end of an expression
use std::io;
to import libraries (called the prelude)- variables are immutable by default, use
mut
before the variable name //
for comments, use//
for each line of multi-line comments- In
String::new()
,::
refers to an associated function of the string type that makes a new value (a growable UTF-8 string) .read_line(&mut guess)
=>&
means the argument is a reference (immutable by default)Result
values are an enum that can have multiple variants:Ok
andErr
. Errors are handled by calling theexpect
method on the result type, if it has an error it gets handled here, otherwise returns the value thatOk
is holding{}
can be used as placeholders inprintln!
println!("x = {x} and y + 2 = {}", y + 12);
=>x = 5 and y = 14
- number types default to
i32
32 bit integer
use rand::Rng
=>Rng
trait defines methods that are implemented by random number generators. A trait must be in scope in order to use its methodssecret_num = rand::thread_rng().gen_range(1..=100);
- =>
rand::thread_rng
is the function with the rng to be used local to the current thread of execution gen_range
method takes a range exp as an arg, is inclusive on lower and upper bounds
- =>
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("Spot on!!"),
}
- A
match
expr is comprised of "arms", each arm has a pattern to match against and the code to run if matched- expr ends after first successful match
- Variables are immutable by default, add
mut
in front of var to make it mutable =>let mut x = 123;
- Can't use
mut
with constants =>const x = 5;
- Constants can only be set to a constant expression, not a result from a value at runtime
const SOME_CONST: u32 = 14
- Can't use
- Shadowing variables is allowed, and encouraged for transforming types, mutating in inner scopes.
- use
let variable = "some new value"
to declare a new variable with the same name for transformations, while keeping the "original" variable immutable- using
mut
instead will not allow you to change the type, and actually mutates the value
- using
- "Shadowed" value is used by compiler until scope ends or a new shadowing overrides it
- use
- Rust is a statically typed langauge =>
some_var: u32 = //...
for type annotations (here, unsigned 32 bit integer) - Scalar type represents a single value. Rust has four primary scalar types:
- Integer
- Floating-point number
- Booleans
- Characters
Integer Type: Number without a fractional component
- signed int types start with
i
instead ofu
(signed means it's possible for the int to have a negative value) - the num like
u32
refers to the number of bits of storage it uses - can use
_
as a visual separator =>10_000
equals10000
- If value exceeds typed range => Integer Overflow
- Int overflow results in panic in debug mode, but wrapping in release mode (should be handled, reliance is considered an error)
Floating-Point Types
- Two types =>
f32
andf64
, refers to bits of size for each floatf32
is single precisionf64
is double precision
- All floats are signed
Numeric Operations
+
,-
,*
,/
(division),%
(remainder) all operate as expected and evaluate to a single value that is bound to a variable
Boolean Type
true
andfalse
, 1 byte in size- e.g.
let f: bool = false;
Character Type char
- Rusts' must primitive alphabetic type, four bytes in size (a unicode scalar value)
- Use
' '
single quotes (compared to string literals which use" "
double quotes) - e.g.
let my_string: char = 'abc';
- Group multiple values into a single type
Tuple Type
- Group a variety of types into one compound type
- Fixed length
- Create using a comma separated list of values in parenthesis e.g.
- a "unit" is a tuple without any values, written as
()
to represent an empty value or return type
fn main() {
let tup: (i32, f64, u8) = (100, 6.9, 9)
// can be destructured using pattern matching
let (x, y, z) = tup
println!({y}) => 6.9
// or zero-based index access
let nine = tup.2 // => 9
}
Array Type
- Unlike a tuple, every element of an array must have the same type
- Fixed length (use a Vector type instead for collection that can grow or shrink in size)
- Used for data to allocated to stack rather than heap
- Value accessed by index in brackets
let first = a[0]
- A value for an index out of the defined bounds will cause panic at runtime (memory safety)
fn main() {
let a: [i32; 3] = [1,2,3]; // 3 element array of i32 ints
let b = [3; 5]; // 3 element array, each with the value 5 => [5, 5, 5]
}
fn main() {}
=> entrypoint to most rust appsfn
keyword to declare new functions- Rust doesn't care about order that functions are defined, only that they're in scope
- Must declare type of each parameter
- Func return values are not named, but must be declared after an arrow
->
Function bodies are comprised of a series of statements optionally ending in an expr
- Statements: Instructions that perform some action and do not return a value
- e.g. creating and assigning a var, func definitions
- Expressions: Evaluate to a resultant value
- e.g. calling a func or a macro, a new scope block using
{}
- expressions do not include ending semicolons
- putting an ending semicolon at the end of an expr transforms it into a statement (no return value)
- e.g. calling a func or a macro, a new scope block using
-
if
and loops are the most common control flows -
if
expr- Blocks of code associated with the conditions are called arms
- Will not automatically type convert non-boolean types to a bool
-
Rust has three kinds of loops:
loop
,while
,for
loop
endless loop until explicit use ofbreak
to stop- use
continue
to skip over rest of code in block for this iteration and move to next iteration break
andcontinue
by default apply to the innermost of nested loops- Can use loop labels to disambiguate multiple loops:
'some_loop_label: loop {}
(notice the'
prefix)- Can affect labelled loops
break 'some_loop_label;
(notice the'
prefix)
- Can affect labelled loops
- Can use loop labels to disambiguate multiple loops:
- use
while
loop automaticallybreak
s when condition is no longertrue
for
loop to execute some code for each item in a collection- e.g.
for element in some_array { println!("Value is: {element}"); }
- e.g.
Understanding Ownership: Ch 4
-
Ownership is Rust's most unique feature, providing memory safety guarantees without a garbage collector
- A set of rules that govern how a Rust program manages memory
-
Stack: Values are stored (pushed) in order received and removed (popped) in reverse (last in first out)
- Good for data with a known size
-
Heap: Less organized, need to request space from memory allocator to get a pointer to the memory address of the location
- Slower for read and write, because have to allocate memory, also have to follow a pointer for reads
-
Ownership Rules:
- Each value in Rust has an owner
- There can only be one owner at a time (assigning a value to another variable moves it)
- When the owner goes out of scope, the value will be dropped
-
String
type (not string literal) is mutable:let s = String::from("hello world");
-
drop
function is called automatically at the closing curly brace to drop memory allocation for vars going out of scope -
reassignment of variables results in a "move" not a shallow copy because first variable is invalidated:
let s1 = String::from("some string");
let s2 = s1; // invalidates s1
println!("{}", s1) // throws error
- Prepending
&
in front of a type signifies a reference is passed- Allows referring to a value without taking ownership of it (called borrowing)
- References are immutable by default
- Can create a mutable reference by declaring the initial var with
mut
and&mut
as the reference- Can't mix both
&s
immutable and&mut s
mutable references
- Can't mix both
- Can either have 1 mutable reference, or limitless immutable references, but can't mix them
- Refernces must always be valid, or compiler will err (if reference is to a value that has been dropped)
fn calculate_length(s: &String) -> usize { // note &String type is immutable ref
s.len()
}
let mut s = String::from("hi"); // var declared as mutable
change(&mut s); // mut reference passed
fn change(some_string: &mut String) { // mut param declared
some_string.push_str(", other string");
}
- A slice is a kind of reference that lets you refer to a contiguous sequence of elements in a collection
&str
is the type for "string slice" which is an immutable referencefn returns_string_slice(s: &String) -> &str { ...
takes String referrence, returns a string slice- Generally more flexible to use
(s: &str)
as a param, becuase can be used for&String
and&str
values
- Can slice other collections like arrays more generally too
// String slice
let s = String::from("hack days!");
let slice = &s[0..4]; // "hack" => slice var s from index 0 to 3 (start index to one more than end index)
let slice = &s[..4]; // also "hack"
let slice = &s[6..11] // "days!"
let slice = &s[6..] // also "days!"
let slice = &s[..] // "hack days!" => from start index to one more than end index
// Array slice
let a = [1, 2, 3];
let arr_slice = &a[..3]; // [1, 2]
- Struct is a custom data type that allows you to package and organize related values as named fields that comprise a meaningful group
- Methods are associated functions
- Can instantiate structs, use dot notation to retrieve values
- Use
mut
to declare mutable instance => entire struct must be mutable, can't allow mutating only specific fields - Can use factory functions to build structs with default values
// Define struct with field names and types
struct User {
active: bool,
first_name: String, // Instance owns this value
email: String,
sign_in_count: u64,
}
// Instantiation of mutable instance with concrete values
let mut user1 = User {
active: true,
first_name: String::from("Will"),
email: String::from("abc@example.com"),
sign_in_count: 1,
};
user1.email = String::from("someotheremail@place.com"); // dot notation for field access
// Factory to build user with default values
fn build_user(email: String, first_name: String) -> User {
User {
active: true,
first_name, // Field init shorthand notation to use parameter value with same name
email,
sign_in_count: 1,
}
}
// Using struct update syntax to create a new instance spreading values from other struct (for values not explicitly set)
let user2 = User {
email: String::from("newemail@example.com"),
..user1 // spread must come last (will not use the value for email because it was explicitly defined above)
};
- Tuple Structs Without Named Fields behave like tuples
struct Color(i32, i32, i32); // unnamed fields because would be redundant
let black = Color(0, 0, 0); //
- Similar to functions, but defined within a struct (or enum/trait object) and pirst parameter is always
self
self
is the instance of the struct the method is being called on
- Define a method within the context of a struct by starting an
impl
(Implement) block for the struct - Can define a method with the same name as one of the struct's fields which can be invoked by using
()
parenthesis- e.g.
rect1.width()
for method,rect1.width
for field value - Generally used to create getter methods (not implemented automatically) => helpful for access control
- e.g.
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// within impl block, `Self` is an alias for the type the block is for
fn area(&self) -> u32 { // `&self` is short for `self: &Self`
self.width * self.height
}
}
- All functions defined within an
impl
block are associated functions (associated with the type named in theimpl
)- Can define non-method associated functions (don't have
self
as first param) - Used like
String::from
- Can define non-method associated functions (don't have
impl Rectangle {
fn square(size: u32) -> Self { // -> returns an instance of Rectangle with matching width and height
Self {
width: size,
height: size,
}
}
}
let new_square = Rectangle::square(10);
-
Enums allow defining a type by enumerating its possible variants
- A way of saying "a value is one of a possible set of values"
- An enum value can only be one of its variants
-
Option
enum expresses that a value can be something or nothing -
Pattern matching in
match
expr for running code based on the value of an enum -
Rust does not have a null type, instead has
Option
- To use an
Option<T>
, need to handle both theNone
andSome
cases or compiler will fail
- To use an
enum Option<T> { // `<T>` is a Generic type, can hold any type
None,
Some(t),
}
if
requires condition to evaluate to a boolean, butmatch
can take any typematch
has 'arms' that have a pattern and some code- Matches must be exhaustive
- Use
other
as a catch all pattern matcher where value is used (must be last) e.g.other => func_with_param(other)
- Use
_
as a special pattern that matches any value and does not bind to it e.g_ => no_param_func()
- Use
_ => ()
to match any value and do nothing (expr is an empty tuple)
- Use
- Use
fn plus_five(x: Option<i32>) -> Option<i32> { // Return value could be None, or a 32 bit integer
match x {
None => None, // If value is `None`, return `None`
Some(i) => Some(i + 5), // the `i` binds to the value contained in `Some`
}
}
let six = Some(6); // 6
let eleven = plus_five(six); // 11
let none = plus_five(None); // None
- Can avoid some of the boilerplate code of a match when only interested in handling code that matches one pattern and ignoring the rest:
let config_max = Some(3u8);
if let Some(max) = config_max { // Only does something if matches the `Some(max) = config_max` expr
// do something
}
- Packages => A Cargo feature to build, test and share creates
- Crates => A tree of modules that produces a library or executable
- The smallest amount of code that a Rust compiler considers at a time
- Crates can be:
- Binary (with a
main
func, compile to an executable)$ /src/main.rs
by default, or$ /src/bin/
for multiples - Library (no
main
, used for shared functionality)$ /src/lib.rs
- Binary (with a
- For grouping and reusing related code
- All items are private to parent modules by default (use
pub
to make an item public)- Making a module public with
pub mod
doesn't make its contents public => need topub fn
to expose the funcs
- Making a module public with
- Modules and use => Allows controlling the organization, scope and privacy of paths
use crate::some_module::Type
to load in the type so later can refer to justType
pub mod garden;
=> load in thesrc/garden.rs
file
- Paths => A way of naming an item like a struct, func or module
- Use
::
as separators - Absolute (full path starting at root)
- Relative (starts from current module, and uses
self
,super
or an identifier in the current mod)
- Use
- Can use
pub
to designate struct as public, but fields remain private- Make individual fields for a struct public with
pub field_name: String,
- If an enum is public, all its variants become public
pub enum Appetizer {...}
- Make individual fields for a struct public with
use
is like creating a symlink in the filesystem, allows avoidance of qualifying full paths to modules- add
use crate::some_module::the_module
to bringthe_module
into scope as if it were defined there (still checks privacy)
- add
- idiomatic to
use
the parent module of a func to be used so that it must be invoked withparent_mod::the_func
so it's clear it's not locally defined - idiomatic to
use
the fully qualified path to structs, enums and other items as
for aliasing duplicate names =>use std::io::Result as IoResult;
pub use
to re-export names so that calling code can bring into scope- Nested paths for re-using shared paths:
use std::{cmp::Ordering, io};
=> bringsstd:cmp::Ordering
andstd::io
into scopeuse std::io::{self, Write};
=> bringsstd::io
andstd::io::Write
into scopeuse std::collections::*;
=> Glob operator brings all public items fromstd::collections
into scope
- Vector for storing more than one value with all values next to each other in memory (Link to Documentation)
- Data is stored on the heap
- Can only store values of the same type
- Can't hold an (immutable) reference to an element and modify the vector within the same scope (violates mutable and immutable references in same scope rule)
let v: Vec<i32> = Vec::new(); // explicit type for empty Vector
let v2 = vec!['a', 'b', 'c'] // implied type using `vec!` macro
v2.push('d'); // push item into vector => ['a', 'b', 'c', 'd']
let first = v2.get(0); // gets item at zeroth index
let first_directly = &v[0]; // panics if outside of vector range
let mut v3 = vec![1, 2, 3];
for i in &mut v {
*i += 100; // use * dereference operator to get the value in `i` before mutating
// note: can't modify number of elements in vector within for loop
}
-
Enum to store multiple types in a Vector
- Create an enum with the types that might be present, then populate the vector with that single enum type
- The compiler will ensure each case is handled
- Create an enum with the types that might be present, then populate the vector with that single enum type
-
String
String
orstr
(string slice)String
is implemented as a wrapper around a vector of bytes with some differences, so many similarities in the operations available to strings and vectors- Strings are UTF-8 encoded, so multibyte chars are supported
- Can't access strings via indexes e.g.
s[0]
to get first char- Becuase of UTF-8 encoding, some chars are multibyte and index will not always result in a valid unicode scalar
- 3 ways to look at strings from Rust => bytes, scalar values, grapheme clusters (like letters)
- Can use byte ranges, but dangerous because might land in middle of a multi-byte char
let s = &hello[0..4];
(first 4 bytes) - Best way to iterate over strings is to specify chars() or bytes()
let s = String::from("another string"); // `String` from string literal
let s2 = "some string".to_string(); // working on string literal directly
let mut s3 = "hello".to_string();
s3.push_str(" world"); // => "hello world"
// format! macro utilizes refernces, so s2 and s3 are still in scope
let s4 = format!("{s2} and {s3}") // "some string and hello world"
for char in "hi".chars() {
println!("{char}");
}
// => "h"
// => "i"
for byte in "hi".bytes() {
println!("{byte}");
}
// => 104
// => 105
- Hash Maps
HashMap<K, V>
- Keys can be any type, but all keys must be of the same type and all values must be of the same type
- Data is stored on the heap
- For types that implement the
Copy
trait likei32
, values are copied into hash map - For owned values like
String
, the values will be moved and the hash map will become the owner of the values - Can use
entry()
to retrieve a value for a key
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 20);
scores.insert(String::from("Yellow"), 30); // will overwrite previous value for "Yellow" key
let team_name = String::from("Blue");
// handle option by using copied() to get an Option<i32> instead of Option<&i32>
// `unwrap_or` to set score of 0 if no entry in hashmap for key
let score = scores.get(&team_name).copied().unwrap_or(0);
println!("Blue team score: {score}"); // Blue team score: 10
for (key, value) in &scores {
println!("{key}: {value}"); // prints in arbitrary order
}
println!("{:?}", scores); // {"Blue": 10, "Yellow": 30}
scores.entry(String::from("Green")).or_insert(100); // either take score for key "Green", or insert 100
- Rust does not have exceptions, it has two major error categories:
- Recoverable =>
Result<T, E>
type - Unrecoverable => symptom of bug, calls
panic!
macro
- Recoverable =>
panic!
=> Default: print failure message, unwind, clean up stack and quit- Can configure to immediately abort instead and let the OS clean up the memory (unwind/clean up is expensive)
[profile.release] panic = 'abort'
- set
$ RUST_BACKTRACE=1 cargo run
to print out the backtrace
<T>
and<E>
are generic types<T>
returned in success case withOk
variant<E>
returned in failure case withErr
variant
- Can use
unrwrap_or_else
method to condense logic instead of usingmatch
and handlingErr
unwrap
method provides shorthand where if theResult
value isOk
, will return the value insideOk
, but if it's anErr
variant will call thepanic!
macroexpect
method is similar tounwrap
, but allows setting a custom error message
- Propogate errors by returning the error to the calling code
Err(e) => return Err(e)
?
operator as a shortcut to propogate errorsFile::open("some.txt")?;
(note the?
after the Result)?
passes through thefrom
function, so can convert the error returned to a custom Err- Can chain method calls after
?
to lessen boilerplate code - Can only use
?
where return type is compatible (i.e. aResult
,Option
type, or implementsFromResidual
)
enum Result<T, E> {
Ok(T),
Err(E),
}
fn some_func() -> Result<String, io::Error> {
let mut some_file = File::open("file.txt")?; // note the `?` where a Result is returned
}
panic!
in tests, prototype code, code examples- use
unwrap
orexpect
when hardcoding means a result will always be ok, but compiler won't be able to tell panic!
when code gets into a bad state from unexpected value- return a
Result
when failure is an expected possibility - Functions have contracts: behaviour is only guaranteed if the inputs meet requirements => panic when contract is violated
- Rely on type system to avoid excess handling
- generics: abstract stand-ins for concrete types or other properties
- Functions can take params of generic types
- Use traits to define behavior in a generic way
- Combine traits + generics to constrain generic types to only accept types with specified behaviors
- lifetimes: a variety of generics that tell the compiler how references relate to each other
- So compiler can know enough about borrowed values to ensure refs will be valid in more situations
- Use generic types to de-duplicate logic by extracting to a function with generic parameters
- by convention, generic type parameter for a function is
<T>
- No runtime cost to using generic types, compiler converts to concrete types (process called monomorphization)
// "the function largest is generic over some type `T`
fn largest<T>(list: &[T]) -> &T {...}` // accepts reference to a slice of values of generic type `<T>`
// works for structs too
struct Point<T> {
x: T,
y: T, // x and y are of generic type, but must be of the same type T
}
struct Point<T, U> { // defining two generics allows x and y to mix/match between two types
x: T,
y: U,
}
// Works to hold generic types in enum variants too
enum Option<T> {
Some(T), // some holds value for a generic (any) type
None, // holds no value
}
enum Result<T, E> { // holds multiple generics, T and E
Ok(T),
Err(E),
}
- trait defines functionality that a type has and can share with other types
- Used to share behavior in an abstract way
- Use trait bounds to specify that a generic type can be any that has a certain trait behavior
- need to bring trait into scope to use =>
use aggregator::{Summary, Tweet};
- Can't implement traits on external traits on external types (orphan rule)
pub trait Summary { // declare trait with name
fn summarize(&self) -> String; // note semi-colon, each type implementing this trait must have a method with this exact signature defined
}
impl Summary for NewsArticle { // note `for`
fn summarize(&self) -> String { // ...some implementation for NewsArticle }
}
impl Summary for Tweet {
fn summarize(&self) -> String { // ... some implmentation for Tweet}
}
Can also define default implmentations
pub trait Summary {
fn summarize(&self) -> String { // default implmentation in block
String::from("(Read more...)")
}
}
// can define functions that accept types with traits
pub fn notify(item: &impl Summary) { // accepts any type that implements the `Summary` trait
println!("Breaking news! {}", item.summarize()); // calls method from Summary trait
}
// Trait bound syntax forces multiple traits to conform to same trait
pub fn notify<T: Summary>(item1: &T, item2: &T) {
// Multiple trait bounds to enforce implementations of multiple traits with `+`
pub fn notify<T: Summary + Display>(item: &T) { // accepts types that implement both Summary and Display traits
// Can return types that implement traits in method return definition (as long as only returning a single type)
fn returns_summarizable() -> impl Summary { // return value is guaranteed to implement the Summary trait
-
Lifetimes are another type of generic that ensure references are valid as long as they're needed to be
- Every reference has a lifetime => the scope where that reference is valid
- Their goal is to prevent dangling references
- Generally lifetimes are implicit and inferred, but must be annotated when the lifetimes of references could be related in multiple ways
- Annotating lifetimes is a concept that it pretty unique to Rust
-
Lifetime annontation syntax describe the relationships of the lifetimes of multiple references to each other
- names of lifetime parameters must start with an apostrophe
'
, and are lowercased and short - The lifetime of the reference returned is the same as the smaller of the lifetimes of the references passed in
- i.e. they both go out of scope when the smaller lifetime goes out of scope
- Lifetime Elision rules allow the compiler to sometimes generate lifetimes implicitly to avoid boilerplate code
'static
lifetime denotes that the reference can live for the entire duration of the program
- names of lifetime parameters must start with an apostrophe
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // the returned reference will be valid as long as both params are valid (share lifetime 'a)
if x.len() > y.len() {
x
} else {
y
}
}
// Lifetime declared as part of struct definition
struct Excerpt<'a> { // an instance of Excerpt can't outlive the reference it holds in its `part` field
part: &'a str,
}
- Setup, run code under test, assert results are what's expected
- A Rust test is essentially a function that's annotated with the
test
attribute - Run tests with
$ cargo test
, each test is run in a new thread - use
#[should_panic(expected = "some expected panic message")]
attribute above test to assert that panic occured in test code - Write unit tests in the
/src
dir directly in the file with the code it's testing (conventionally within amod tests
)
#[cfg(test)] // tells cargo to compile and run the code only when running `$ cargo test` but ignore when running `$ cargo build`
mod tests {
use super::*; // to bring code under test defined in outer module into the scope of inner `tests` module
#[test] // annotation attribute indicates that below it is a test fn
fn it_works() {
let result = 5 + 5;
assert_eq!(result, 10) // assert_eq! is a macro from std lib
assert_ne!(result, 11) // assert not equal
}
}