Monday, May 29, 2023

Rust Error Handling

 



In this post we will review Rust error handling methods and best practice.

The first method to handle errors is to use the panic macro. Notice that once panic was run, there is no standard way to recover from it (putting aside the catch_unwind method). The panic macro will stop the program, and unwind through the call stack, release all memory. An example for panic usage follows below.


extern crate core;


fn end_of(text:String) -> String {
println!("checking the text {text}");
if !text.contains(" "){
panic!("no end of string related work here");
}
let (_,end_of_string) =text.rsplit_once(" ").unwrap();
return String::from(end_of_string);
}

fn main() {
println!("{}", end_of( String::from("Hello World!")));
println!("{}", end_of( String::from("NoSpacesHere")));
}


The output of this example is:


checking the text Hello World!
World!
checking the text NoSpacesHere
thread 'main' panicked at 'no end of string related work here', src/main.rs:7:9
stack backtrace:
0: rust_begin_unwind
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library/std/src/panicking.rs:579:5
1: core::panicking::panic_fmt
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library/core/src/panicking.rs:64:14
2: guessing_game::end_of
at ./src/main.rs:7:9
3: guessing_game::main
at ./src/main.rs:15:19
4: core::ops::function::FnOnce::call_once
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


Note that we have a detailed stack trace, in addition to the error. 


While panic is good for test and examples, in production code we would probably need to avoid terminating our process for each problematic issue. The recommended method is using the Result enum.



extern crate core;

use std::fs::File;
use std::io;
use std::io::Read;

fn end_of(text: String) -> String {
println!("checking the text {text}");
if !text.contains(" ") {
panic!("no end of string related work here");
}
let (_, end_of_string) = text.rsplit_once(" ").unwrap();
return String::from(end_of_string);
}

fn tail_file(file_path: String) -> Result<String, io::Error> {
let my_file = File::open(file_path);
match my_file {
Ok(mut file_handler) => {
let mut file_data = String::new();
let read_result = file_handler.read_to_string(&mut file_data);
return match read_result {
Ok(_) => Ok(end_of(file_data)),
Err(e) => Err(e)
};
}
Err(e) => {
Err(e)
}
}
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("{}", tail_file(String::from("a.txt"))?);
println!("{}", tail_file(String::from("none existing file"))?);
Ok(())
}



The output of this example is:


checking the text aaa bbb ccc
ccc
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }


In the example above we've used some of the Result manipulation methods. 

First, the tail_file function returns a Result enum typed with String and io::Error. 

Second, any call to a function that can return error is following with match arm expression to handle errors.

Third, the main function is using '?' to panic upon errors, but to enable this, we must change the main signature to return Result with a trait.


The tail_file can be also simplified to use the '?' :


fn tail_file(file_path: String) -> Result<String, io::Error> {
let mut file_data = String::new();
File::open(file_path)?.read_to_string(&mut file_data)?;
Ok(end_of(file_data))
}



Notice that in case of error by a result, we are totally blind to the stack trace, which is a very bad practice. To get a full stack of the error from a result, we can choose using the error_stack crate.




Monday, May 22, 2023

Rust Collections

 




This post contains short cheat-sheets examples for the collections: vectors, strings, and hashmap.


Vectors


// vector without initial value must have the type specified
let v1: Vec<i32> = Vec::new();
// type of vector with initialization is automatically derived
let mut v1 = vec![1, 2, 3];

// add values
v1.push(72);

// direct access to element
let second_item = v1[1];
println!("second item is {second_item}");

// direct access will get error if index of of bounds
// let non_existing = v1[9999];


// using get, returns an option
let non_existing = v1.get(9999);
match non_existing {
Some(x) => println!("it's there {x}"),
None => println!("you went too far"),
}

let cell_pointer = &v1[0];
v1.push(73);
/*
immutable borrow error here
after adding an element to the vector, we might had to reallocated space and move the array data
hence the cell pointer is no longer valid
*/
// println!("item {cell_pointer}");


// update loop on vector
for item in &mut v1 {
*item += 100;
}

// read only loop on vector
for item in &v1 {
println!("vector item {item}");
}


Strings

// new empty String
let mut s1 = String::new();


// String from str alternatives
let data = "One method";
let s2 = data.to_string();
let s3 = String::from("Another method");

// updating a string
let mut updated_string = String::from("start");
updated_string.push_str(" and end");
updated_string.push('!');
println!("updated string to {updated_string}");

// concatenation
let con1 = String::from("Foo");
let con2 = String::from("Bar");
let con3 = String::from("!");
let con4 = format!("{con1} {con2} {con3}");
println!("concatenation is {con4}");
let con5 = con1 + " " + &con2 + " " + &con3;
// borrow of moved value error in con5 creation
// println!("concatenation origin {con1}");

// due to unicode issues, we don't access string chars like this s[2]
for c in "יו".chars() {
println!("char {c}");
}
// notice this will be more than 2 bytes, due to unicode
for b in "יו".bytes() {
println!("byte {b}");
}


Hash Maps

use std::collections::HashMap;
let mut workers = HashMap::new();
workers.insert("Alice", 45);
workers.insert("Bob", 34);

// get with default
let alice_age = workers.get("Alice").copied().unwrap_or(0);
let no_one_age = workers.get("No one").copied().unwrap_or(0);
println!("alice is {alice_age} years old, and no one is {no_one_age}");

// loop
for (name, age) in &workers {
println!("worker {name} is {age} years old");
}

// update values
workers.insert("Alice", 46);
let alice_age = workers.get("Alice").copied().unwrap_or(0);
println!("alice is {alice_age} years old now");

// update values only if new key
workers.entry("Alice").or_insert(99);
let alice_age = workers.get("Alice").copied().unwrap_or(0);
println!("alice is (still) {alice_age} years old");

/*
count words by hashmap, while updating values.
notice that we get a pointer to the value, and we update it directly
*/
let mut words = HashMap::new();
for word in "my name is indigo montoya you have killed my father prepare to die".split_whitespace() {
let count = words.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", words);




Monday, May 15, 2023

Rust Modules


In this post we review how to organize a big project into modules and files.

Let review the files structure:




We have two root standard files: The main.rs is the binary starting point and the lib.rs is the library starting point. We can have both, so the crate can be used both as binary and as library.


Each module should (but not must) exists in it own file. Sub modules should be in a sub folder named after their parent module. We have a module named another_game, and its sub module named player.


Let's review usage of the modules in the main.rs file.




First we can see a module within the main.rs, and not in it own file. This is a bad practice, but is possible.

We can access the modules using relative path and absolute path. Looks like the relative path is the more sensible method. A relative path can also access its parent using the super keyword. 

Anything that we want to access from outside the module must be defined as public. Notice that for structs, in case not all the fields are public, we cannot instantiate them, so we must supply a constructor function. Notice that to access an item from outside its module, we must have all the route from the accessing point to the item public.


Let view some of the module code in another_game.rs:




Here again we see the public keyword wherever we want to provide public access. We can see that a struct can have some of its fields public, and some private.







Monday, May 8, 2023

Rust Structures and Enums




 


Structures

Structures can hold multiple fields of multiple types.


struct Animal {
name: String,
legs: u32,
}


Instances of structures can be created, and modified:


let mut insect = Animal {
name: String::from("insect"),
legs: 12,
};

insect.legs = 6;


Builder function can be used, and we can use defaults for fields name to be as the variable names:


fn produce_6legs_animal(name: String) -> Animal {
return Animal {
name,
legs: 6,
};
}

fn main() {
let ant = produce_6legs_animal(String::from("ant"));
}


Move fields from another structure. Notice that move of values from structure means that some of the fields are no longer valid:


fn print_animal(animal: &Animal){
println!("the animal {} has {} legs", animal.name, animal.legs)
}

fn main() {
let caterpillar = Animal {
name: String::from("caterpillar"),
legs: 12,
};

print_animal(&caterpillar);

let butterfly = Animal {
legs: 6,
..caterpillar
};

print_animal(&butterfly);

// compile error - value borrowed here after partial move
print_animal(&caterpillar);
}


Unnamed struct can also be used. Notice the instance creation is using regular parenthesis:


struct Position(String, i32, i32);

fn main() {
let start = Position(String::from("start"), 0, 0);
}


Structs without any fields can also be used:


struct MyTrait;

fn main() {
let my_trait = MyTrait;
}


Moving on to Objects

Structures are used for object oriented design. Using structs and impl blocked we can create multiple constructors and methods. Notice that we can have multiple impl blocks.


struct Person {
name: String,
age: i32,
}

impl Person {
fn new_baby(name: String) -> Self {
return Person {
name,
age: 0,
};
}
fn is_very_old(&self) -> bool {
self.age > 120
}
fn is_older_than(&self, other: &Person) -> bool {
self.age > other.age
}
}


Using the objects is simple.

Again, to avoid move of ownership use pointers for structure when sending it as parameter.


fn main() {
let noah = Person {
name: String::from("Noam"),
age: 950,
};
let einstein = Person {
name: String::from("Einstein"),
age: 76,
};
let baby = Person::new_baby(String::from("Herman"));


let description = if noah.is_very_old() { "very old" } else { "young" };

println!("the person {:?} is {}", noah.name, description);
if noah.is_older_than(&einstein) {
println!("Noah is older than Einstein")
}
}


Debugging 

By adding #[derive(Debug)] attribute to a structure we can print it in a single line using {:?}, and in multiple lines using {:#?}. In addition we can use the dbg! macro to print code position and variable information.

let baby = Person::new_baby(String::from("Herman"));
println!("single line print {:?}", baby);
println!("multiple lines print {:#?}", baby);
dbg!(&baby);

The output is:

single line print Person { name: "Herman", age: 0 }

multiple lines print Person {
name: "Herman",
age: 0,
}

[src/main.rs:26] &baby = Person {
name: "Herman",
age: 0,
}



Enums

Enums can be just a list, or even hold properties similarly to structs.
In addition, enums can be used in match arms.

use crate::Furniture::{Closet, Table};

#[derive(Debug)]
enum Furniture {
Chair,
Table,
Closet { doors: u32 },
}

impl Furniture {
fn open(&self) {
match self {
Closet { doors } => {
println!("open {doors} doors");
}
_ => (
println!("no need to open")
)
}
}
}

fn main() {
let chair = Furniture::Chair;
let closet = Closet { doors: 2 };
println!("{:?}", chair);
println!("{:?}", closet);

chair.open();
closet.open();
}

Option


A special enum is Option, which can hold a value, and can also contain None.
Notice that match expression must be exhaustive, that is - hold all possible values: Some and None.


fn square(i: Option<i32>) -> Option<i32> {
return match i {
Some(x) => Some(x * x),
None => None,
};
}

fn main() {
let two = Some(2);
let none = None;

// prints Some(4)
println!("{:?}", square(two));
// prints None
println!("{:?}", square(none));
}

We can also check value of an option using "if let" statement.

if let Some(i) = two {
println!("the actual value of the option is {}", i)
}



Redis Dynamic Memory Analzyer


 


In previous posts, I've displayed examples for Trie implementation in GO, and for how to analyze Redis memory usage. In this post we will combine both to a dynamic analyzer of redis memory usage.


First we will change the Trie to keep some Redis related information within each of the Trie node:


package trie

import "sort"

type NodeData struct {
Count int
Size int64
NotPrintedSize int64
NotPrintedCount int
TotalSize int64
}

type Scanner func(path []rune, node *Node)

type Node struct {
value rune
Data *NodeData
Children map[rune]*Node
}

func ProduceTrie() *Node {
return &Node{
Children: make(map[rune]*Node),
Data: &NodeData{},
}
}

func (n *Node) AddNode(values []rune, size int64) {
if len(values) == 0 {
n.Data.Count++
n.Data.Size = size
return
}

childName := values[0]
child, exists := n.Children[childName]
if !exists {
child = ProduceTrie()
child.value = childName
n.Children[childName] = child
}

child.AddNode(values[1:], size)
}

func (n *Node) TraverseDepthFirst(scanner Scanner) {
n.traverseDepthFirst(scanner, nil)
}

func (n *Node) traverseDepthFirst(scanner Scanner, path []rune) {
var newPath []rune
if path == nil {
newPath = make([]rune, 0)
} else {
newPath = make([]rune, len(path)+1)
for i, value := range path {
newPath[i] = value
}
newPath[len(path)] = n.value
}

sorted := n.sortedNodes()
for _, name := range sorted {
child := n.Children[name]
child.traverseDepthFirst(scanner, newPath)
}

scanner(newPath, n)
}

func (n *Node) sortedNodes() []rune {
sorted := make([]rune, 0)

for name := range n.Children {
sorted = append(sorted, name)
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i] < sorted[j]
})
return sorted
}


Notice that each trie node represents one letter, so the redis key "abc" is represented by 3 nodes.

Each of the trie node holds information about how many keys are ending in this node, which is equal to 0 in case it is only in the middle of a key (for example "a", and "b" nodes for key "abc"), and equals to 1 in case it is the end of the key (for example "c" node for key "abc"). 

In addition, each node holds information about the TotalSize of all it children recursively. 

Now we we build the trie by scanning the redis keys.


type Analyzer struct {
redisApi redisapi.RedisApi
root *trie.Node
printFraction float32
report []string
}

func ProduceAnalyzer(
redisApi redisapi.RedisApi,
printFraction float32,
) *Analyzer {
return &Analyzer{
redisApi: redisApi,
printFraction: printFraction,
root: trie.ProduceTrie(),
}
}

func (a *Analyzer) AnalyzeMemory(
keysPrefix string,
) string {
keys := a.redisApi.Keys(keysPrefix)
progressLog := progress.ProduceProgress(len(keys), "scan keys")

totalMemory := int64(0)
for _, key := range keys {
keyMemory, _ := a.memoryUsageSafe(key)
totalMemory += keyMemory
a.root.AddNode([]rune(key), keyMemory)
progressLog.Increment()
}

a.buildDataBottomUp()
return a.getSummary()
}

func (a *Analyzer) memoryUsageSafe(key string) (size int64, wrappedError error) {
defer errsimple.WrapWithError(&wrappedError)
size = a.redisApi.MemoryUsage(key)
return size, wrappedError
}

func (a *Analyzer) buildDataBottomUp() {
a.root.TraverseDepthFirst(a.buildDataScanner)
}

func (a *Analyzer) buildDataScanner(_ []rune, node *trie.Node) {
node.Data.TotalSize = node.Data.Size

for _, child := range node.Children {
node.Data.TotalSize += child.Data.TotalSize
}
}

func (a *Analyzer) getSummary() string {
a.root.TraverseDepthFirst(a.printScanner)
return strings.Join(a.report, "\n")
}

func (a *Analyzer) printScanner(path []rune, node *trie.Node) {
totalSizeAll := float32(a.root.Data.TotalSize)
printThreshold := int64(totalSizeAll * a.printFraction)

anyChildPrinted := false
node.Data.NotPrintedSize = node.Data.Size
node.Data.NotPrintedCount = node.Data.Count
for _, child := range node.Children {
node.Data.NotPrintedSize += child.Data.NotPrintedSize
node.Data.NotPrintedCount += child.Data.NotPrintedCount
if child.Data.NotPrintedSize == 0 {
anyChildPrinted = true
}
}

if node.Data.NotPrintedSize == 0 {
return
}

if !anyChildPrinted && node.Data.NotPrintedSize < printThreshold {
return
}

nodePercentNotPrinted := 100 * float32(node.Data.NotPrintedSize) / totalSizeAll
nodePercentAll := 100 * float32(node.Data.TotalSize) / totalSizeAll
line := fmt.Sprintf(
"%9v keys, size excluding %6.3f%%, size including %6.3f%% %s",
node.Data.NotPrintedCount,
nodePercentNotPrinted,
nodePercentAll,
string(path),
)
a.report = append(a.report, line)

node.Data.NotPrintedSize = 0
node.Data.NotPrintedCount = 0
}



The real power of this analyzer is in its dynamic representation of the memory. The analyzer builds a Trie representation of all the keys in redis with a specified prefix, but prints the Trie by a fraction value. The fraction value indicates which nodes should be printed, for example if the fraction value is 0.01, the summary includes only keys whose cumulative memory is over 1% of the redis memory. This allows a dynamic view of the redis memory, and enables a gradual analysis of the keys, while aiming to find the problematic keys.


Example of output is:


11 keys, size excluding  4.343%, size including  4.343%   aaak1
11 keys, size excluding 4.343%, size including 4.343% aaak2
11 keys, size excluding 4.343%, size including 4.343% aaak3
11 keys, size excluding 4.343%, size including 4.343% aaak4
11 keys, size excluding 2.997%, size including 2.997% aaak501
11 keys, size excluding 3.670%, size including 3.670% aaak50201
11 keys, size excluding 3.670%, size including 3.670% aaak50202
11 keys, size excluding 3.670%, size including 3.670% aaak50203
11 keys, size excluding 3.670%, size including 3.670% aaak50204
11 keys, size excluding 3.670%, size including 3.670% aaak50205
11 keys, size excluding 3.670%, size including 3.670% aaak50206
11 keys, size excluding 3.670%, size including 3.670% aaak50207
11 keys, size excluding 3.670%, size including 3.670% aaak50208
11 keys, size excluding 3.670%, size including 3.670% aaak50209
2 keys, size excluding 0.581%, size including 33.609% aaak5020
10 keys, size excluding 2.722%, size including 36.330% aaak502
11 keys, size excluding 2.997%, size including 2.997% aaak503
11 keys, size excluding 2.997%, size including 2.997% aaak504
11 keys, size excluding 2.997%, size including 2.997% aaak505
11 keys, size excluding 2.997%, size including 2.997% aaak506
11 keys, size excluding 2.997%, size including 2.997% aaak507
11 keys, size excluding 2.997%, size including 2.997% aaak508
11 keys, size excluding 2.997%, size including 2.997% aaak509
2 keys, size excluding 0.642%, size including 60.948% aaak50
10 keys, size excluding 3.945%, size including 64.893% aaak5
11 keys, size excluding 4.343%, size including 4.343% aaak6
11 keys, size excluding 4.343%, size including 4.343% aaak7
11 keys, size excluding 4.343%, size including 4.343% aaak8
11 keys, size excluding 4.343%, size including 4.343% aaak9
1 keys, size excluding 0.367%, size including 100.000% aaak










Monday, May 1, 2023

Rust Ownership

 


In this post we will review the "ownership" in Rust.


Rust should be as fast as C/C++. This means it must not use garbage collector to manage the memory, but it want to avoid the while memory allocation/free hell. To bypass this, Rust uses ownership. Ownership means that only one variable owns the allocated memory, and the allocated memory is automatically freed by Rust upon exit from the variable scope.


fn play() {
let s1 = String::from("Hello World!");
println!("{s1}")
// s1 is freed upon end of the scope
}


But, this means you cannot have 2 owners of the same variable.

fn play() {
let s1 = String::from("Hello World!");

// move ownership to s2
let s2 = s1;
println!("{s2}");

// compile error - s1 "borrowed after move"
println!("{s1}");
}


To fix that we can use clone():

fn main() {
play()
}

fn play() {
let s1 = String::from("Hello World!");

let s2 = s1.clone();
println!("{s2}");

println!("{s1}");
}



Notice that this is also relevant for functions - once we send a variable to a function, the ownership moves to the function, and we no longer can use it.

fn play() {
let s1 = String::from("Hello World!");

// move ownership to play_too
play_too(s1);

// compile error - s1 "borrowed after move"
println!("{s1}");
}

fn play_too(text: String) {
println!("{text}");
}


We can use the clone() as before, but we can also send the variable by reference. This means we still own the variable, but the function can still use it. The variable will be freed upon the original scope end.

fn play() {
let s1 = String::from("Hello World!");

// send by reference
play_too(&s1);

println!("{s1}");
}

fn play_too(text: &String) {
println!("{text}");
}


Notice that we can also send it as mutable variable, but we must explicitly specify it when calling the function.

fn play() {
let mut s1 = String::from("Hello World!");

// send by reference
play_too(&mut s1);

println!("{s1}");
}

fn play_too(text: &mut String) {
text.push_str(" And Goodbye...");
println!("{text}");
}


Notice that Rust blocks dangling pointers, so in case the variable is freed, we cannot return a pointer to it:

fn play() -> &String {
let s = String::from("Hello World!");
// compile error: no value for it to be borrowed from
return &s;
}



First Steps in Rust





In this post we will review how to get familiar with Rust programming language.


Useful resource is "The Book", that is the Rust Programming Language book.


Installation

To install Rust, use the following:


curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh


The latest version we're using in this post is 1.69.0. In case of need, rust version can be updated using:


rustup update


Using Rust requires us to use the cargo tool, which is a build tool and a package manager.

Create a New Project

Let start with a new project:


cargo new hello-world


I am using JetBrains GoLand IDE with Rust plugin installed to edit the code, and run the project, but in general, we can run the project using:


cargo run


To add dependencies, add in the project manifest file, Cargo.toml the required libraries:

[dependencies]
ferris-says = "0.3.1"


and now, we can use the dependency:


use ferris_says::say;
use std::io::{stdout, BufWriter};

fn main() {
let stdout = stdout();
let message = String::from("Hello World!");
let width = message.chars().count();

let mut writer = BufWriter::new(stdout.lock());
say(&message, width, &mut writer).unwrap();
}


The output is:




Dependencies

The file cargo.lock specific all transitive dependencies used by the project. The cargo.lock file is created upon the first build, and it updated when updating the dependencies in the cargo.toml file.

To add a dependency, add under the [dependencies] section in the cargo.toml the library needed and its version. Cargo will look for the highest version without API change. For example, version 1.2.3 in the cargo.toml might eventually be resolved to 1.2.9 in the cargo.lock, but not to 1.3.0.

Once entered to the cargo.lock file, dependencies versions are no longer updated. To force update of the lock file, use:


cargo update


To create documentation for the project and all its dependencies, use:


cargo doc --open


Functions

functions:

fn my_function(){

}


Call to function:

my_function();


Call to macro (notice the '!' at the end):

println!()


Function with return value should not include ';' in the return statement (the last statement):

fn add(i1: i32, i2: i32) -> i32 {
i1 + i2
}


alternatively use explicit return statement:

fn add(i1: i32, i2: i32) -> i32 {
return i1 + i2;
}


Variables

Mutable variables:

let non_mutable_variable = 5;
let mut mutable_variable = 4;

// compile error
non_mutable_variable = 8;
// but this one is ok
mutable_variable = 8;


Sending variables by reference:

io::stdin().read_line(&mut guess)


Variable shadowing (useful for convert between types):

let i=5;
let i=String::from("fff");


Constants must include the type as part of the declaration.

const COUNT: u32 = 33;


Tuples

Create and use tuple:
let my_tuple = (1, 'a', "bb");
let first = my_tuple.0;
let third = my_tuple.2;
println!("my tuple elements are {first} {third}")

The empty tuple is called unit:
let empty_tuple =();

Arrays

Fixed length arrays, whose size cannot be changed.
let my_array = [0, 1, 2, 3, 4, 5];

Arrays can be initialized to the same value for all items:
let array_of_many_fives = [5; 100];

Arrays types can be explicitly configured:
let mut explicit_type_array: [i32; 5] = [0; 5];
explicit_type_array[0] = 1

Notice that out of array bounds access is checked in runtime (unlike C)

Ranges

range inclusive

1..=100

range exclusive

1..100

Reversed range:

(0..10).rev()


Loops

While loop:

let mut i = 0;
while i < 10 {
i += 1;
}


Loop on range:

for i in 0..10 {
println!("{i}");
}


Loops and break:

loop {
if 1 == 1 {
break;
}
}


A loop can also return values:

let the_answer = loop {
if 1 == 1 {
break 42;
}
};
println!("{the_answer}")


Conditional Expressions

if statements can return a value, for example as one line initialization:
let number = 5;
let other_number = if number > 5 { "big" } else { "small" };
println!("{}", other_number);

Results and ARM

Handling Result type:

let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("numbers only please");
continue;
}
};


arm: pattern to match against, and code to run:

match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}


IO Library

to use the library:

use std::io;


Print macro with variables:

println!("You guessed: {guess}");