Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust Programming

Self Introduction

Who am I?

  • Learned programming in 1983 BASIC, Forth, Assembler.
  • Self-employed since 1999.
  • Teaching Perl, Python, Rust and also Linux, git, GitHub, GitLab, CI, Docker.
  • Introducing testing and CI to companies.

  • Open Source projects.
  • Organizing Meetups.

Who are you?

  • Which programming languages do you know and to what extent? (e.g. year or level)

  • Which operating systems do you use for writing code, and which to runnig code?

  • What kind of applications are you planning to write in Rust?

Introduction to Rust

Why Rust?

The main web site of the Rust the language lists 3 main topics:

  • Performance
  • Reliability
  • Productivity

Why Rust - Performance

What is written in Rust?

Why Rust - Reliability

  • type system

  • memory safety

  • thread-safety

  • Dangling pointers

  • Data races

  • Buffer overflow

  • Integer overflow

  • Iterator invalidation

  • Accessing invalid addresses in memory

  • Sead-locks

  • memory leaks

  • double-free

Why Rust - Productivity

  • Tooling (IDEs, compilation, package management, dependency management, linter).
  • Documentation.
  • 3rd party open source libraries in the Crates registry.

Major features of Rust

  • Shift left programming (eliminate bugs earlier in the development process).

  • Speed and Open Source community.

  • Corporate support, Linux kernel

  • Error handling: exceptions VS return values VS returning Result in Rust.

  • Eliminate null issue.

Rust Jobs

Memory allocation

  • Stack
  • Heap
  • In the binary

Rust Books

Publishers

Crates (3rd party libraries)

  • Crates.io - the registry of all the crates.
  • Docs.rs - the documentation of all the crates.
  • Lib.rs - Lightweight, opinionated, curated, unofficial alternative to crates.io.
  • Blessed.rs - A hand-picked list of crates.
  • Rust Digger - analyzing crates and recommending improvements.

Rust exercises with feedback

Podcast, newsleter

Other Rust learning resources

Rust in other languages

Articles about Rust

Rust community

Demo None handling

One of the nice features of Rust is the way None (null, undefined, etc.) values are handled. Having variables with undefined values is a major issue in any programming language. In some languages if you declare a variable, but don't initialize it then there is garbage in the variable, in others there is a special symbol called one of these names: None/null/undefined. Compilers might be happy with this and you will only find yourself in trouble during runt-time when the code tries to access the content of this variable. If this code is in some conditional, then it might happen rarely and only at an hour extremely inconvenient for your support team.

Let's see how Rust deals with this issue?

In this example the first row is just some instruction to make the linter happy in this contrived example. You can disregard it for now.

In the main function We declare a variable called answer without assigning any value to it. Later, most likely after several lines of code, we assign a value to it and try to print it.

This code works. There is not problem with this.

#![allow(clippy::needless_late_init)]

fn main() {
    let answer;

    // ...

    // What if we try to print the answer here already? Isn't it undefined, or null?
    // println!("The answer is {answer}");

    answer = 42;
    println!("The answer is {answer}");
}

However, what would happen if we enabled the println! statement before the assignment to answer?

Use variable before initialization?

#![allow(clippy::needless_late_init)]

fn main() {
    let answer;

    // ...

    // What if we try to print the answer here already? Isn't it undefined, or null?
    println!("The answer is {answer}");

    answer = 42;
    println!("The answer is {answer}");
}

In this case when Rust compiles the code it will notice that we are trying to use a variable before we initialized it:

$ cargo build
   Compiling no-null v0.1.0 (/home/gabor/work/rust.code-maven.com/books/rust-programming/src/examples/introduction/no-null-compilation-error)
error[E0381]: used binding `answer` is possibly-uninitialized
 --> src/main.rs:9:29
  |
4 |     let answer;
  |         ------ binding declared here but left uninitialized
...
9 |     println!("The answer is {answer}");
  |                             ^^^^^^^^ `answer` used here but it is possibly-uninitialized
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0381`.
error: could not compile `no-null` (bin "no-null") due to 1 previous error

Demo Option to handle None

Let's see another, probably a bit more realistic, example of this situation.

In this example when we declare the variable input we also initialize it to None.

Then we collect the values from the command line into a variable called args. Let's disregard the details of this for now. I wanted to create a working example for the None-handling and this is the simplest I managed to do.

If there are more than 1 values in that args variable, meaning if the user supplied at least one value after the name of the program then we assign this value to the input variable. However, we don't just assign the value as it arrived from the user, we wrap it in the Some() expression. It might look like a function, but it is not really one. It just wraps the value. We'll discuss the details later.

So now, after the if-statement we either have None in the input variable or we have Some(value) in there.

#![allow(clippy::needless_late_init)]

fn main() {
    let mut input = None;

    let args = std::env::args().collect::<Vec<String>>();
    if args.len() > 1 {
        input = Some(&args[1]);
    }

    match input {
        Some(thing) => {
            println!("The input is {thing}");
        }
        None => {
            println!("There was no input")
        }
    }
}

In order to use it this variable properly Rust makes us check if the content of the variable is None or Some(value). Rust has many nice ways to do this, probably the clearest is using he match pattern-matching statement.

We know that the input variable has two possible states hence we need two "arms" in the match statement. One will be triggered if the variable contains Some(value) the other when the variable contains None. In both cases we can provide a block of code to be executed.

In our example we provided a variable called thing, yes that is a variable we can use any name there. The value provided by the user that was later wrapped in the Some() construct will be assigned to this variable for the duration of the block after the fat-arrow.

Usage

We can provide the command-line parameters right after we typed in cargo run:

$ cargo run
There was no input

$ cargo run hello
The input is hello

Some extra notes

  • In this example we had to use the mut keyword to make the variable mutable.
  • In this case, because each arm of the match statement has only one statement (a println! in both cases) we could get-by without the curly braces.

Demo None handling with Option

This is another example to demonstrate an Option that can be either None or Some(value).

fn main() {
    let name = get_name(true);
    say_hi(name);

    let name = get_name(false);
    say_hi(name);
}

fn get_name(good: bool) -> Option<String> {
    if good {
        Some(String::from("Gabor"))
    } else {
        None
    }
}

fn say_hi(name: Option<String>) {
    match name {
        Some(text) => println!("Hello {text}"),
        None => println!("Welcome! My name is Rust."),
    }
}

See original idea on What is Rust and why is it so popular?


  • Option
  • match
  • Some
  • None

Error handling

How can a function indicated that it encountered a problem?

Exception

In some languages the code raises an exception (or throws an exception) that some code higher up the call stack is expected to catch using some try-code. This is said to be rather expensive and of course if at some point we forget to handle the exception then the whole program might crash.

Return status code

In other languages the function returns a value indicating if there was an error. The programmer needs to make sure the caller can easily separate error codes from real return values. There are various solutions for this, for example passing references and returning the data by changing the reference or if the languages allows for it returning multiple values. (e.g. the first value is the status indicating success or the type of error and the second value being the real generated value.)

While this is doable in most languages this is convention not enforced by the language and by the compiler.

This also means that every return value to every function call must be inspected.

Return a Result

The way this is solved in Rust is that functions that might fail can be defined to return a Result enum. This can have either a real value wrapped in Ok() if the call was successful or an error message wrapped in Err() if there was an error.

The caller then is forced to check if the returned value indicates success or error using the match keyword.

This is built-in the language and thus make it easier to handle it in the same way in every code-base.

In order to avoid the need to inspect the returned value of every function call, Rust also allows us to make the call automatically short-circuit the current function when an a call returns an error. This makes it easy to "catch" the errors only higher up in the call-stack without paying the price incurred by exception handling.

Demo error handling with Result

Let's see how in Rust a function reports an error condition and how it can be handled by the caller.

The first line in the read_files function std::fs::read_to_string will read in the content of a file and it is expected to return it as a string. However things can go wrong. The file might not exist or the person running the program might have not rights to read the file. How does the function report the problem?

This function returns a Result enum that is either the content of the file wrapped in Ok() or some type representing an error wrapped in Err(). (Although Ok() and Err() look like function calls they are called the variants of the Result enum.)

We can't directly use the returned value we must somehow check which one of the variants was returned and we need to extract the content of the variant. We can do this with match or one of its many alternatives.

In this example you can see the same code twice. Once trying to read a file that does not exist and once trying to read a file that does.

fn main() {
    read_files();
}

fn read_files() {
    let result = std::fs::read_to_string("other.md"); // Result<String, Error>

    match result {
        Ok(content) => {
            println!("content length: {}", content.len());
        }
        Err(err) => {
            println!("Error: {err}");
        }
    }

    println!();

    let result = std::fs::read_to_string("src/main.rs"); // Result<String, Error>

    match result {
        Ok(content) => {
            println!("content length: {}", content.len());
        }
        Err(err) => {
            println!("Error: {err}");
        }
    }
}

The output looks like this:

Error: No such file or directory (os error 2)

content length: 573

In the first case we printed the content of Err() which is the error message we got from the system-call trying to access the file.

In the second case we got the length of the string which is the length of the file.

Always checking the returned value?

It would be rather annoying if for every function call at every level of the call-stack we would nee to check the status of the result.

However, this isn't the case. Rust allows us to automatically propagate the result up to the caller if it was an Err(). This is what we can see in the next example.


  • Result
  • match
  • Ok
  • Err

Demo error handling with Result and question mark

In this example we set up the read_files function to return a Result enum that can contain either nothing (()) or a "boxed error". Don't worry about the details of this for now. Suffice to say that the read_files function itself now is expected to return a Result.

That means in the main function now we should check, using match, if it was a success (returning an OK() that contains nothing represented by an empty pair of parentheses, in which case we do nothing, or if it was an Err() in which case we print the error message.

Inside the read_files function we could remove all the match expressions and added a question-mark ? at the end of the read_to_string function calls. This operator will make Rust check if the returned value is Ok(). If it is then the expression will return the actual content without the Ok() wrapper. So in this case the variable content will already contain the content of the file. If the returned value is Err() then Rust will arrange for the read_files function to immediately return this same error wrapped in Err().

Basically if there was an error it passes up on the call-stack, if it was successful then it peals off the OK() wrapper and gives us the value we were looking for.

fn main() {
    match read_files() {
        Ok(()) => (),
        Err(err) => {
            println!("Error: {err}");
        }
    }
}

fn read_files() -> Result<(), Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string("other.md")?;
    println!("content length: {}", content.len());
    println!();

    let content = std::fs::read_to_string("src/main.rs")?;
    println!("content length: {}", content.len());

    Ok(())
}

Running the code will result in this output:

Error: No such file or directory (os error 2)

This is different from the previous one as we also changed the semantics of the code. So I might need a better example to demonstrate this, but I think you get the idea of how Rust handles errors and how we can eliminate a lot of the boilerplate code.


  • Result
  • dyn
  • Box

Error handling

This is probably a better example about error handling. It needs explanation.

b.txt

0

c.txt

20

fn main() {
    for file in ["a.txt", "b.txt", "c.txt"].iter() {
        divide(100, file);
    }
}

fn divide(dividend: i32, filename: &str) {
    let divisor_result = std::fs::read_to_string(filename);
    // Result<String, Error>

    match divisor_result {
        Ok(content) => {
            let content = content.trim();
            match content.parse::<i32>() {
                Ok(divisor) => {
                    if divisor == 0 {
                        println!("Cannot divide by zero in file {}", filename);
                    } else {
                        println!("{} / {} = {}", dividend, divisor, dividend / divisor);
                    }
                }
                Err(_) => {
                    println!("Failed to parse number in file {}", filename);
                }
            }
        }
        Err(err) => {
            println!("Error: {err}");
        }
    }
}
Error: No such file or directory (os error 2)
Cannot divide by zero in file b.txt
100 / 20 = 5

fn main() {
    for file in ["a.txt", "b.txt", "c.txt"].iter() {
        match divide(100, file) {
            Ok(result) => println!("100 / divisor = {}", result),
            Err(err) => println!("Error: {}", err),
        }
    }
}

fn divide(dividend: i32, filename: &str) -> Result<i32, String> {
    let content = std::fs::read_to_string(filename)
        .map_err(|e| format!("Failed to read file {}: {}", filename, e))?;

    let divisor = content
        .trim()
        .parse::<i32>()
        .map_err(|_| format!("Failed to parse number in file {}", filename))?;

    if divisor == 0 {
        return Err(format!("Cannot divide by zero in file {}", filename));
    }

    Ok(dividend / divisor)
}


fn main() {
    for file in ["a.txt", "b.txt", "c.txt"].iter() {
        match divide(100, file) {
            Ok(result) => println!("100 / divisor = {}", result),
            Err(err) => println!("Error: {}", err),
        }
    }
}

fn divide(dividend: i32, filename: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string(filename)?;

    let divisor = content.trim().parse::<i32>()?;

    if divisor == 0 {
        return Err("Division by zero is not allowed".into());
    }
    Ok(dividend / divisor)
}
Error: No such file or directory (os error 2)
Error: Division by zero is not allowed
100 / divisor = 5

Hello World

Install Rust

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

See in the Rust book

rustup --version

rustup 1.26.0 (5af9b9484 2023-04-05)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.70.0 (90c541806 2023-05-31)`
rustc --version

rustc 1.70.0 (90c541806 2023-05-31)
$ rustc -vV
rustc 1.88.0 (6b00bc388 2025-06-23)
binary: rustc
commit-hash: 6b00bc3880198600130e1cf62b8f8a93494488cc
commit-date: 2025-06-23
host: x86_64-unknown-linux-gnu
release: 1.88.0
LLVM version: 20.1.5

Editor and IDE

You can use any editor or IDE to write Rust code, but there are a few that stand out.

I personally use VS Code with the approprite plugins and vim without anything.

Let me know your favorite setup!

Hello World in Rust!

Let's start with the traditional "Hello World!" example. In this case we are going to use the Rust compiler directly just so you will see it yourself, but then we'll quickly switch to the commonly used wrapper around it called cargo.

  • Rust files must have an extension of .rs. Both the main file that we run and the libraries.
  • The name of the file does not matter for now, but let's call it main and let's put it in the src folder to make it easier to switch to cargo.
  • The main Rust file must have a function called main.
  • We declare functions using the fn keyword.
  • Rust uses curly braces like most of the C-like languages to denote blocks of code. (Unlike Python that uses indentation or Ruby that uses end.)
  • In order to print to the screen we use the println! macro. It looks like function call, but we know it is a macro because of the exclamation mark !. It generates some Rust code during compilation.
fn main() {
    println!("Hello World!")
}

We user the Rust compiler called rustc to compile the code. It will generated a file called main on Linux and macOS or main.exe on MS Windows.

rustc src/main.rs

We can take this platform and architecture dependent executable and run it on its own.

./main

  • fn
  • main
  • println!

Hello World in Rust with Cargo

Earlier we saw how to compile a Rust program using the compiler directly, but in reality Rust developers rarely interact with the compiler directly. Instead they use cargo the all helper tool.

Cargo is the package management, dependency management, and build system of Rust. It is extendable and thus a lot of additional tools are combined with it.

For now let's see how we get started using it:

  • cargo new first-app creates a new folder called first-app
  • With a file called Cargo.toml
  • A folder called src and a file src/main.rs with the hello world code.
  • cargo run will compile the code and run it.
cargo new first-app
cd first-app
fn main() {
    println!("Hello, world!");
}
[package]
name = "first-app"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
  • Unless you do this in a folder that is already under git, the new command will also create a git repository using this folder.

Compile and run in one step

Once we are in the folder of our brand new Rust application we can type in cargo run. This will compile the code in this folder and run it. In one step. Just as you would do in any of the dynamic languages. You don't have to deal with the separate compilation and running step and you don't need to create an maintain a Makefile either.

$ cargo run
   Compiling first-app v0.1.0 (/home/gabor/work/slides/rust/examples/first-app)
    Finished dev [unoptimized + debuginfo] target(s) in 0.34s
     Running `target/debug/first-app`
Hello, world!

The compilation phase created a file called Cargo.lock and a folder called target.

  • Cargo.lock is used to freeze the versions of all the dependencies of the project. We have none so far. We'll see this more in detail later on.
  • The target folder contains sub-folder called debug and that contains the compiled file and all the temporary files that Rust needed for the compilation.

  • cargo

Cargo build

Executing cargo run will first compile your code including the build of an executable and then it will run it. In some cases you might only want do the compilation (and executable building) phase. You can do that by typing in:

cargo build

This will check if you already have an compiled binary in the target folder newer than your source code. If the binary does not exist or the source code is newer then it will build the binary. By default it will build in debug mode and will place the executable in the target/debug folder.

If the name of the crate is first-app as in the previous example then the cargo build command will generated the target/debug/first-app file on Linux and macOS and the target/debug/first-app.exe on MS Windows.

You can run this manually as

./target/debug/first-app

Release mode

Alternatively you could do the compilation and build the executable in release mode by providing the --release flag.

cargo build --release

This will probably take longer as it involves optimizations. You will probably do this rarely. This will create an executable in the target/release folder.

In both cases the filename of the executable is the value of the package.name field in the Cargo.toml file.

For the previous example it would generated the target/release/first-app file.

You can then run this as

./target/release/first-app

  • build
  • target
  • --release

Run in release mode

Though you'll probably rarely need this, you can also ask cargo to compile and run in one step in release mode:

cargo run --release

Use of macros with parentheses, square brackets, or curly braces

In our first example we used the println! macro with parentheses: println!() that made it look like a function call.

Rust allows us to use macros with square brackets or with curly braces.

In most cases people use regular parenthese, but for example the vec! macro is usually used with square brackets.

You can even leave spaces between the exclamation mark and the opening brace, but I don't think I have seen that. I'd probably avoid that.

The default formatting style of Rust is having no space when parenthese or square brackets are used and having a single space when curly brace is used.

fn main() {
    println!("Hello, world!");
    println!["Hello, world!"];
    println! {"Hello, world!"};
}

Rust and comments

Both single-line and multi-line comments are available in Rust. Comments are also used to write user documentation. See the spec of comments for more details.

fn main() {
    println!("hello");
    // println!("Foo");

    /*
        This is a
        multiline comment
    */

    println!("world"); // comments
}

  • //
  • /* */

Rust - Hello Foo

We use the let keyword to declare a varible. Thouh we don't have to, we can already assign a value to it using a single equal sign = as it is done in most programming languages.

In order to print to the sceeen we can use a pair of curly braces {} as a placeholder for the content of a variable in the println! macro. Behind that macro is another one called the format! macro where you can read more about the various options.

fn main() {
    let name = "Foo";
    println!("Hello {}, how are you?", name);
}

The output will look like this:

Hello Foo, how are you?

  • let
  • println!
  • format!

Interpolation

Since Rust 1.58, so for quite a ling time, we can also include the name of a variable in the curly braces. This feature is called interpolation. It usually make the expression more readable.

fn main() {
    let name = "Foo";
    println!("Hello {name}, how are you?");
}

The output:

Hello Foo, how are you?

Printing a variable

One thing that annoys me about println! macro is that we cannot just put a variable in it and get it printed. We need to include a formatting string with placeholders for the values or with interpolation as we saw it earlier.

The following code will NOT compile. At least the compiler will give you a semi-understandable hint how to fix it.

fn main() {
    let name = "Foo";
    println!(name);
}

The error message:

error: format argument must be a string literal
 --> examples/intro/formatting_required.rs:3:14
  |
3 |     println!(name);
  |              ^^^^
  |
help: you might be missing a string literal to format with
  |
3 |     println!("{}", name);
  |              +++++

error: aborting due to previous error

Coming from languages such as Perl and Python I used do way too much debugging by sprinkling print-statements around the code. Even in those languages after a while, as the program grows I always switch to logging, but still.

Given that Rust is a compiled language where the compilation takes a lot longer than in Perl or Python, this strategy is less useful, but still, I'd like my simple printing for debugging back. Luckily Rust provides it.

Printing a string fixed

The immediate fix for the reported error is to include the formatting string and/or to use the interpolated version.

fn main() {
    let name = "Foo";
    println!("{}", name);
    println!("{name}");
}

Result:

Foo
Foo

Debugging print

For debugging purposes, however, there is a better solution. Instead of using the `println!' one could use the dbg! macro. Not only will it print the content of the variable, it will also print the name of the file, the line number and the name of the variable.

The only drawback, or maybe that is another advantage?) might be that everything is being printed to to the Standard Error channel STDERR.

fn main() {
    let name = "Foo";
    dbg!(name);
}

Output on STDERR:

[src/main.rs:3:5] name = "Foo"

  • dbg!

Rust and print

A little overview of the built-in print-like macros:

  • print! to STDOUT.
  • println! to STDOUT with a trailing newline.
  • eprint! to STDERR.
  • eprintln! to STDERR with a trailing newline.
  • dbg! to STDERR with bells and whistles.
  • format! returns formatted string. This is behind the printing macros.
#[allow(clippy::print_with_newline)]

fn main() {
    let fname = "Foo";
    let lname = "Bar";

    println!("{} {}", fname, lname);
    println!("{fname} {lname}");
    print!("{fname} {lname}\n");

    eprintln!("error in {} {}", fname, lname);
    eprint!("error in {} {}\n", fname, lname);

    let full_name = format!("{fname} {lname}");
    println!("{full_name}");
    dbg!(full_name);
}

Output:

Foo Bar
Foo Bar
Foo Bar
error in Foo Bar
error in Foo Bar
Foo Bar
[examples/intro/print.rs:14] full_name = "Foo Bar"

  • print!
  • println!
  • eprint!
  • eprintln!
  • dbg!
  • format!

Exercise: Hello World

  • Make sure you have Rust installed.
  • Try rustc --version.
  • Create a new project with Cargo.
  • Write a program that prints "Hello World".
  • run the program.
  • Add comments to the program and run it again.
  • Extend the program to have your name in a variable and print that too.

Variables

Variables are immutable

We declare variables using the let keyword, and despite calling them variables, in Rust they are immutable by default and the compile enforces this.

fn main() {
    let num = 2;
    println!("{num}");

    num = 3;
    println!("{num}");
}

This is the error message:

error[E0384]: cannot assign twice to immutable variable `num`
 --> src/main.rs:5:5
  |
2 |     let num = 2;
  |         ---
  |         |
  |         first assignment to `num`
  |         help: consider making this binding mutable: `mut num`
...
5 |     num = 3;
  |     ^^^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
error: could not compile `immutable-number` (bin "immutable-number") due to 1 previous error

  • let

Number in a mutable variable

We can make a variable mutable by adding the mut keyword to the declaration. In this example we use numbers as strings have addition constraints.

fn main() {
    let mut num = 2;
    println!("{num}");

    num += 1;
    println!("{num}");
}

The output:

2
3

  • let
  • mut

Literal strings in variables are immutable

Similar to numbers, if we declare a variable and assign a string to it as in the following example, that variable is going to be immutable.

fn main() {
    let text = "Hello World!";
    println!("{text}");

    text = "Something else";
    println!("{text}");
}

We get the following compilation error:

error[E0384]: cannot assign twice to immutable variable `text`
 --> src/main.rs:5:5
  |
2 |     let text = "Hello World!";
  |         ----
  |         |
  |         first assignment to `text`
  |         help: consider making this binding mutable: `mut text`
...
5 |     text = "Something else";
  |     ^^^^^^^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
error: could not compile `immutable-string` (bin "immutable-string") due to 1 previous error

What happens if we make the variable mutable?

Literal string in a mutable variable can be replaced

In this case too we can use the mut keyword to mark a variable as mutable. This will allow us to replace the string by another string.

fn main() {
    let mut text = "Hello World!";
    println!("{text}");

    text = "Something else";
    println!("{text}");
}

Output:

Hello World!
Something else

A little about memory

As we start talking about string we encounter the question of memory management. Something we probably have not thought about when writing Perl, Python, PHP or Ruby code and something that is also different from both C and C++.

If we declare a variable and assign a string in double-quotes to it, that string is going to be compiled into the binary excutable and will remain there. That means we cannot actually change the content of that string.

In other words, literal strings are embedded in the binary.

We can change the variable to point to another string embedded in the binary, but that does not change the string itself.

A really mutable string

If we would like to have a string that can really be change we need to make sure or code will allocate some space in the memory where we can really change things.

For this Rust provides a type called String.

In this example we take a literal string which is going to be embedded in the binary and using the String::from function we create a copy of the string in an area of the memory (in the heap) that we can actually change. We also need to mark the variable to be mutable using the mut keyword if we really want to change it.

The push_str method will append another string to the one we have.

fn main() {
    let mut name = String::from("Foo");
    println!("{name}");

    name.push_str(" Bar");
    println!("{name}");
}

Output:

Foo
Foo Bar

  • String
  • from
  • push_str

A literal string cannot be changed

If we go back to the variable that was holding a literal string, you can see that while we could replace the content of the variable there are no methods allowing us to change the string. Eg. push_str does not exist for literal strings.

fn main() {
    let mut name = "Foo";
    println!("{name}");

    name.push_str(" Bar");
}

Compilation error:

error[E0599]: no method named `push_str` found for reference `&str` in the current scope
 --> examples/variables/change_literal_string.rs:5:10
  |
5 |     name.push_str(" Bar");
  |          ^^^^^^^^ method not found in `&str`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0599`.

  • push_str

Cannot change the type of a variable

So far when declared variables we have not declared the types of the variables, but behind the scenes Rust maintaines a type-system and enforces it. When we first assign a value to a variable - usually when we declare the variable - Rust will infere the type of the variable from the value we assigned to it.

Then when we try to change the value Rust will make sure we can only change it to a value of the same type.

In this example when we declared the variable answer we assigned a (literal) string to it.

If late we try to assign a number, Rust will stop us with a compilation error.

fn main() {
    let mut answer = "What is the answer";
    println!("{answer}");

    answer = 42;
    println!("{answer}");
}

The compilation error:

error: expected `;`, found `answer`
 --> src/main.rs:3:25
  |
3 |     println!("{answer}")
  |                         ^ help: add `;` here
4 |
5 |     answer = 42;
  |     ------ unexpected token

error[E0308]: mismatched types
 --> src/main.rs:5:14
  |
2 |     let mut answer = "What is the answer";
  |                      -------------------- expected due to this value
...
5 |     answer = 42;
  |              ^^ expected `&str`, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `cannot-change-type` (bin "cannot-change-type") due to 2 previous errors

Redeclare immutable variable - Shadowing

fn main() {
    let name = "Foo";
    println!("{name}");

    let name = "Bar";
    println!("{name}");
}
Foo
Bar

Redeclare immutable variable and change type - Shadowing

The fact that we cannot change the type of a variable is probbaly obvious to people who are arriving to Rust from C, C++, Java, or other strict-typed languages, but it is rather annoying for people who arrive from the dynamically typed languags such as Perl or Python.

There is at least one use-case where it seems natural to want to assign a different type to the same variable name.

When we get an input from the outside world (keyboard, file, network, etc.) at the low level it is always a string. What if we are expecting a number? We need to first get the input. Store it in a variable. Then convert it to a number and store it in.... and at that point we need to come with another variable name which is basically the same as the previous one.

In Rust there is a feature called "shadowing" when we decalre a variable a second time. We effectively hide the first variable from view. It does not get destroyed, it just becomes inaccesible while the new, the shadowing variable is in effect.

This can be useful if you need to make a few changes and then later no more changes.

As we are not really chaning the variable, we don't even need to make it mutable.

In this example we declare the variable answer 3 times using the let keyword. The first two times we assign strings to it the 3rd time it is a number: In order to demonstrate it is really a number I even added 1 to it.

fn main() {
    let answer = "What is the answer";
    println!("{answer}");

    let answer = "42";
    println!("{answer}");

    let answer = 42;
    println!("{answer}");
    println!("{}", answer + 1);
}

Rust is happy to play along:

What is the answer
42
42
43

Convert string to number

Convert string to (integer) number - parse, turbofish

  • i32

  • parse

  • expect

  • ::<>()

  • In the printing we won't see the difference, but we can do numberical operations on numbers.

  • We can either define the expect type next to the variable name where we are assigning to.

  • Or we can use the so-called turbofish operator ::<>() as we do in the second example.

fn main() {
    let text = "23";
    println!("{text:?}");
    println!();

    let number: i32 = text.parse().unwrap();

    println!("{number}",);
    println!("{}", number + 1);
    println!();

    let number = text.parse::<i32>().expect("Could not convert to i32");
    println!("{number}",);
    println!("{}", number + 1);

    let text = "3.14";
    let number = text.parse::<i32>().unwrap();
    //.expect("Could not convert to i32");
    println!("{number}",);
}
"23"

23
24

23
24

Convert string to number that ends with newline

  • trim
fn main() {
    let text = "23\n";
    println!("'{}'", text);
    let number: i32 = text.parse().expect("Could not convert to i32");

    println!("'{}'", number);
    println!("'{}'", number + 1);
}
'23
'
thread 'main' panicked at 'Could not convert to i32: ParseIntError { kind: InvalidDigit }', string_to_int_fail.rs:6:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Convert string to number after removing whitespaces

  • trim
fn main() {
    let text = "23\n";
    println!("'{}'", text);
    let number: i32 = text.trim().parse().expect("Could not convert to i32");

    println!("'{}'", number);
    println!("'{}'", number + 1);
}
'23
'
'23'
'24'

Convert string to float

  • f32

  • Sometimes we are expecting floating point numbers.

fn main() {
    let text = " 2.3 ";
    println!("'{}'", text);
    let number: f32 = text.trim().parse().expect("Could not convert to f32");

    println!("'{}'", number);
    println!("'{}'", number + 1.0);
}
' 2.3 '
'2.3'
'3.3'

Command line arguments - ARGS intro

Command line arguments - args

  • std::env
  • args
  • Vec

At first the code to accept command line parameters might look scary and thus at this point we won't go into the details. However it is nice to be able to accept parameters from the command line to add some interaction to the code. So for now let's just accept the code.

fn main() {
    let args = std::env::args().collect::<Vec<String>>();
    println!("{args:?}");

    if args.len() < 2 {
        eprintln!("Usage: {} param", args[0]);
        std::process::exit(1);
    }
    let param = &args[1];
    println!("{param:?}");

    let number = param.parse::<u32>().expect("We expected a number");
    println!("{number}");
    println!("{}", number + 1);
}

Exercise: Rectangle ARGS

  • Write a Rust application that accepts two values on the command line, the width and the heigh of a rectangle.
  • Prints out the area and the circumference of the rectangle.

Exercise: Circle ARGS

  • Write a Rust application that accetps a number - the radius of a circle.
  • Prints out the area r*r*PI and the circumference 2*r*PI of the circle.

Exercise: basic math operations

  • Write a Rust program that accepts two parameters on the command line and prints out the results of the 4 basic mathc operations (+, -, /, *)
cargo run 10 2
10 + 2 = 12
10 * 2 = 20
10 - 2 = 8
10 / 2 = 5

Solution: Rectangle ARGS

use std::env;
use std::process::exit;

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() != 3 {
        println!("Usage {} length width", args[0]);
        exit(1);
    }
    let width: i32 = args[1].parse().expect("Wanted a number");
    let height: i32 = args[2].parse().expect("Wanted a number");

    let area: i32 = width * height;
    let circumference = 2 * (width + height);

    println!("area: {area}");
    println!("circumference: {circumference}");
}
cargo run

Usage target/debug/rectangle-args length width
cargo run  3 4


area: 12
circumference: 14

Solution: Circle ARGS

#![allow(clippy::approx_constant)]

use std::env;
use std::process::exit;

const PI: f64 = 3.14;

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() != 2 {
        println!("Usage {} radius", args[0]);
        exit(1);
    }
    let radius = args[1].parse::<f64>().expect("Wanted a number");

    let area = radius * radius * PI;
    let circumference = 2.0 * radius * PI;

    println!("area: {area}");
    println!("circumference: {circumference}");
}

Solution: basic math operations

use std::env;
use std::process::exit;

fn main() {
    let args = env::args().collect::<Vec<String>>();
    if args.len() != 3 {
        println!("Usage: {} x y", args[0]);
        exit(1);
    }
    let x: i32 = args[1].parse().expect("Wanted a number");
    let y: i32 = args[2].parse().expect("Wanted a number");

    println!("{x} + {y} = {}", x + y);
    println!("{x} * {y} = {}", x * y);
    println!("{x} - {y} = {}", x - y);
    println!("{x} / {y} = {}", x / y);
}

Development tools

Code formatting

cargo fmt

Clippy

cargo install clippy
cargo clippy

Extreme Clippy

Cargo audit

cargo install cargo-audit
cargo audit

Cargo watch

cargo install cargo-watch

cargo watch -x check

cargo watch -x check -x test -x run

Pre-commit hook

Install pre-commit and then run pre-commit install to configure it on the current.

Examples

Continuous Integration

  • CI

  • GitHub

  • GitHub workflow (GitHub Actions)

Use libraries

Using the Standard library

  • std

  • f32

  • f64

  • consts

  • PI

  • std

fn main() {
    println!("{}", std::f32::consts::PI);
    println!("{}", std::f64::consts::PI);

    // println!("{}", PI); // error[E0425]: cannot find value `PI` in this scope
}
3.1415927
3.141592653589793

Using a single value from the Standard library

use std::f32::consts::PI;
// use std::f64::consts::PI; // error[E0252]: the name `PI` is defined multiple times

fn main() {
    println!("{}", PI);
}
3.1415927

Use (import) a name higher up in the hierarchy

There is also an option to include only part of the path in the use statement. In this case we will have to use the rest of the path in the code, but it will allow us to use multiple name (both PI and E in this case) using the shorter name.

use std::f32::consts::PI;
use std::f64::consts;

fn main() {
    println!("{}", PI);
    println!("{}", consts::PI);
    println!("{}", consts::E);
}
3.1415927
3.141592653589793
2.718281828459045

Using a library with an alias

  • as
  • alias

If for some reason we really need to use the same name from two different libraries, we can use the as keyword to rename one of them for our project. To give it an alias.

use std::f32::consts::PI;
use std::f64::consts::PI as bigPI;

fn main() {
    println!("{}", PI);
    println!("{}", bigPI);
}
3.1415927
3.141592653589793

TODO: Crate library and use local library

  • dependencies

  • path

  • Create a library:

cargo new add-lib --lib

The files created:

{% embed include file="src/examples/libraries/add-lib/Cargo.toml)

#![allow(unused)]
fn main() {
pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}
}
  • Create another crate that will use this library:
cargo new add-app
cd add-app
cargo add --path ../add-lib/

{% embed include file="src/examples/libraries/add-app/Cargo.toml)

//cargo add --path ../add-lib/

fn main() {
    println!("Using the add_lib crate: {}", add_lib::add(2, 3));
}

Rust types

Rust Types

Rust infers the type of variables

  • Infer / deduct the type from the other types of other variables and from the operation on the variable.
  • "guess the type" is probably not a good methaphore as there is no guessing in it.
fn main() {
    let y: i8 = 42; // explicitely set the type
    println!("{y}");

    let x = 42; // defaults to i32
    println!("{x}");

    let z = 42; // at first Rust will assume this is i32, the default

    let result = y + z; // When Rust sees this it will understand that z participates
                        // in a operation where both operands have to be the same type and the other operand
                        // was explicitely set to be i8. So Rust infers that z is also of type i8.
                        // It also infers that "result" will be of type i8.

    println!("{result}");
}

Showing type

  • Using the rust-analyzer will make your IDE show the type. Eg. in Visual Studio Code.
  • A trick that you can also use is to specify some type at the type of assignment, e.g. (), the unit.
  • Then the compiler will complain during compilation.
let x: () = ...
let readdir: () = std::path::Path::new(".").read_dir().unwrap();

Print type of variable

  • type_name

  • type_name_of_val

  • Sometimes during development, during the exploration of Rust it is useful to print the type of a varible.

  • This is one way to do it.

  • It will be added to the stanard library as type_name_of_val.

fn main() {
    let an_integer = 42;
    print_type(&an_integer);

    let a_float = 4.2;
    print_type(&a_float);

    let an_str = "Hello";
    print_type(&an_str);

    let a_string = String::from("World");
    print_type(&a_string);

    let a_vector = vec![3, 4, 5];
    print_type(&a_vector);

    // An iterator
    let readdir = std::path::Path::new(".").read_dir().unwrap();
    print_type(&readdir);
}

fn print_type<T>(_: &T) {
    println!("{:?}", std::any::type_name::<T>());
}
"i32"
"f64"
"&str"
"alloc::string::String"
"alloc::vec::Vec<i32>"
"std::fs::ReadDir"

Numbers

Rust numerical types

  • i8

  • u8

  • i32

  • i64

  • By default integer numbers are stored in i32 that has a range of -2147483648..=2147483647.

  • By default floating point numbers are stored in f64.

  • We can explicitely put numbers in different types.

  • In Python a small integers takes up 28 bytes. Size of integers in Python.

#![allow(clippy::approx_constant)]

fn main() {
    let number = 139871;
    println!("{number}");

    let number: i8 = 100;
    println!("{number}");

    let number: u8 = 255;
    println!("{number}");

    let number = 300_000_000;
    println!("{number}");

    let number: i64 = 3_000_000_000;
    println!("{number}");

    let number = 3.14;
    println!("{number}");

    let number: f32 = 3.14;
    println!("{number}");
}
139871
100
255
300000000
3000000000
3.14
3.14

Numerical operations on integers

  • /

  • %

  • The division keeps the type so dividing one integer by another integer will always return an integer.

fn main() {
    let x = 23;
    let y = 19;
    println!("{x}");
    println!("{y}");

    let add = x + y;
    println!("add: {add}");

    let multiple = x * y;
    println!("multiple: {multiple}");

    let neg = -x;
    println!("neg: {neg}");

    let diff = y - x;
    println!("diff: {diff}");

    let div = x / y;
    println!("div: {div}");

    let modulus = x % y;
    println!("mod: {modulus}");
}
23
19
add: 42
multiple: 437
neg: -23
diff: -4
div: 1
mod: 4

Divide integers and get float

  • as
  • f32
fn main() {
    let x = 8;
    let y = 3;
    let div = x / y;
    println!("{x} / {y} = {div}"); // 8 / 3 = 2

    let div = x as f32 / y as f32;
    println!("{x} / {y} = {div}"); // 8 / 3 = 2.6666667
}
8 / 3 = 2
8 / 3 = 2.6666667

Rust type mismatch in numerical operation

  • i32
  • i64
fn main() {
    let x: i32 = 3;
    let y: i64 = 7;

    let z = x + 1;
    assert_eq!(z, 4);
    println!("{z}");

    let z = y + 1;
    assert_eq!(z, 8);
    println!("{z}");

    //let z = x + y;
    //println!("{z}");
}
  • If we remove the i32 then this works even though the default is i32.
  • That's because Rust will infere the type of the first variable from the type of the second variable and the operation.

Increment integers - augmented assignment

#[allow(clippy::assign_op_pattern)]
fn main() {
    let mut x = 1;
    println!("{x}");

    x = x + 1;
    println!("{x}");

    x += 1;
    println!("{x}");

    // Rust has no prefix and postfix increment operator
    // x++;
    // ++x;
}
1
2
3

unfit in i8 - compile time

// #[allow(overflowing_literals)]

fn main() {
    let x: i8 = 200;
    println!("{x}");
}
error: literal out of range for `i8`
 --> examples/intro/small_integers_unfit_in_i8.rs:2:17
  |
2 |     let x: i8 = 200;
  |                 ^^^
  |
  = note: the literal `200` does not fit into the type `i8` whose range is `-128..=127`
  = help: consider using the type `u8` instead
  = note: `#[deny(overflowing_literals)]` on by default

error: aborting due to previous error

unfit in i8 - run time - overflow

fn main() {
    let mut num: i8 = 126;
    println!("{num}");

    num += 1;
    println!("{num}");

    num += 1;
    println!("{num}");
}
cargo run
126
127
thread 'main' panicked at 'attempt to add with overflow', examples/intro/overflow.rs:6:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  • In debug mode panic!
cargo run --release
126
127
-128

How to find code that might overflow / underflow?

cargo clippy -- --deny clippy::arithmetic_side_effects
error: arithmetic operation that can potentially result in unexpected side-effects

Handle overflow and underflow - saturating

fn main() {
    let mut num: i8 = 126;
    println!("{num}");

    num = num.saturating_add(1);
    println!("{num}");

    num = num.saturating_add(1);
    println!("{num}");
}
126
127
127

Handle overflow and underflow - checked

  • checked_add

  • checked_add

  • Returns and Option that, is either the incremented value wrapped in Some or None.

fn main() {
    let mut num: i8 = 126;
    println!("{num}");

    num = num.checked_add(1).unwrap_or(42);
    println!("{num}");

    num = num.checked_add(1).unwrap_or(42);
    println!("{num}");
}
126
127
42

Handle overflow and underflow - overflowing

  • overflowing_add

  • overflowing_add

  • Returns a tuple: the value (possible after overflow, and a boolean indicating if overflow happened)

fn main() {
    let mut num: i8 = 126;
    println!("{num}");

    let mut ok;

    (num, ok) = num.overflowing_add(1);
    println!("{num} {ok}");

    (num, ok) = num.overflowing_add(1);
    println!("{num} {ok}");
}
126
127 false
-128 true

Handle overflow and underflow - carrying

Handle overflow and underflow - strict

Compare integers

  • cmp

  • Less

  • Greater

  • Equal

  • Ordering

  • We can use the regular <, >, == operators to compare any type of integers assuming the two sides are from the same type.

  • The cmp method returns a value from the Ordering enum.

fn main() {
    let x = 1;
    let y = 2;
    let z = 1;
    println!("{}", x < y);
    println!("{:?}", x.cmp(&y));
    println!("{}", y < x);
    println!("{:?}", y.cmp(&x));
    println!("{}", x < z);
    println!("{:?}", x.cmp(&z));
    println!();

    let x: u8 = 1;
    let y: u8 = 2;
    println!("{}", x < y);
    println!("{:?}", x.cmp(&y));
}
true
Less
false
Greater
false
Equal

true
Less

Compare integers in and if-statement

fn main() {
    let answer = 42;

    if answer > 0 {
        println!("answer = {answer} is greater than 0")
    } else {
        println!("answer = {answer} is less than than 0")
    }
}

Exponent - power

  • pow

  • We can use the pow method to get the exponent of a number, but Rust needs to know the exact type of that number.

  • It can be set explicitly or implicitly as in the case of the function returning an i16 number.

  • We have to be careful as pow can overflow.

fn main() {
    // let x  = 3;
    // println!("{}", x.pow(2));
    // can't call method `pow` on ambiguous numeric type `{integer}`
    // you must specify a type for this binding, like `i32`

    let x: i32 = 3;
    println!("{}", x.pow(2));

    let y: i8 = 10;
    println!("{}", y.pow(2));

    let lucky_number = get_lucky_number();
    println!("{}", lucky_number.pow(2));

    // let y: i8 = 16;
    // println!("{}", y.pow(2));
    // panic!: attempt to multiply with overflow
}

fn get_lucky_number() -> i16 {
    23
}

Exponent protecting against overflow - checked_pow, saturating_pow

  • checked_pow

  • saturating_pow

  • As many other mathematical operations, calling pow can also create a number that does not fit in the expected type and then the code would panic!.

  • We can use the checked_pow that returns an Option

  • It contains the computed value, if successful or None if there was an overflow.

  • An alternative way is to use saturating_pow.

fn main() {
    let x: i8 = 10;
    let y: i8 = 16;

    let z = x.pow(2);
    println!("{}", z); // 100

    // let z = y.pow(2);
    // panic: attempt to multiply with overflow

    let z = x.checked_pow(2).unwrap();
    println!("{}", z); // 100

    // let z = y.checked_pow(2).unwrap();
    // panic: called `Option::unwrap()` on a `None` value

    let z = y.checked_pow(2).unwrap_or(0);
    println!("{}", z); // 0

    let z = match y.checked_pow(2) {
        Some(val) => val,
        None => {
            eprintln!("overflow");
            std::process::exit(1);
        }
    };
    println!("{}", z); // isn't reached if there is an overflow
}

Square root (sqrt)

  • sqrt

  • as

  • f64

  • Calling the sqrt method needs to know the underlying type.

  • It suggests i32, but in fact integers don't have sqrt implemented, only floats.

  • We can convert (cast) an integer to a floating point using the as keyword.

fn main() {
    // let x = 16;
    // println!("{}", x.sqrt());
    // can't call method `sqrt` on ambiguous numeric type `{integer}`
    // you must specify a type for this binding, like `i32`

    // let x: i32 = 16;
    // println!("{}", x.sqrt());
    // // // no method named `sqrt` found for type `i32` in the current scope

    let x = 16;
    let x_square = (x as f32).sqrt();
    println!("The square of {x} is {x_square}");

    let x: f32 = 16.0;
    let x_square = x.sqrt();
    println!("The square of {x} is {x_square}");
}

Square root of integer numbers

  • isqrt

  • integer_sqrt

  • There is a method called isqrt, but it is experimental.

  • There is a crate called integer-sqrt that provides a trait called IntegerSquareRoot and a method called integer_sqrt.

[package]
name = "sqrt-of-integer"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
integer-sqrt = "0.1.5"
use integer_sqrt::IntegerSquareRoot;

fn main() {
    let x = 16;

    println!("{}", x.integer_sqrt());
}

Floating point imprecision

fn main() {
    let a = 0.1;
    let b = 0.2;
    let c = a + b;
    println!("{c}");
    println!("{}", c == 3.0);
}
0.30000000000000004
false

Compare floating point numbers

  • At first we compare two floating point numbers we created.
  • Then we see that the floating point imprecision leads to lack of equality.
fn main() {
    let x = 1.0;
    let y = 2.0;
    let z = 1.0;

    println!("{}", x < y);
    println!("{}", x == z);
    println!();

    let x = 0.1 + 0.2;
    let y = 0.3;
    println!("{}", x);
    println!("{}", y);
    println!("{}", x == y);
}
true
true

0.30000000000000004
0.3
false

rounding float

  • round

  • f64

  • The round method of floating point rounds to the nearest integer.

  • So to round to a specific precision we can multiply-round-divide.

#![allow(clippy::approx_constant)]
#![allow(clippy::unnecessary_cast)]
#![allow(clippy::excessive_precision)]

fn main() {
    let pi = 3.1415926535897932384626433;
    println!("{pi}");
    println!("{:.2}", pi);
    let rpi = (pi * 100.0 as f64).round() / 100.0;
    println!("{rpi}");

    let xpi = (pi * 10000.0 as f64).round() / 10000.0;
    println!("{xpi}");

    println!();

    let x: f32 = 3.14159;
    println!("{x}");
    println!("{:.2}", x);
    let y = (x * 100.0).round() / 100.0;
    println!("{y}");
}
3.141592653589793
3.14
3.14
3.1416

3.14159
3.14
3.14

Compare floating point numbers by rounding

  • round
macro_rules! round {
    ($number: expr, $precision: expr) => {
        (10_u32.pow($precision) as f64 * $number).round() / 10_u32.pow($precision) as f64
    };
}

fn main() {
    let x: f64 = 0.1 + 0.2;
    let y = 0.3;
    println!("{x}");
    println!("{y}");
    println!("{}", x == y);
    println!();

    println!("{}", (100.0 * x).round() / 100.0);
    println!("{}", ((100.0 * x).round() / 100.0) == y);

    println!("{}", round64(x, 2));
    println!("{}", round64(x, 2) == y);

    println!("{}", round!(x, 2) == y);
}

fn round64(number: f64, precision: u32) -> f64 {
    (10_u32.pow(precision) as f64 * number).round() / 10_u32.pow(precision) as f64
}
0.30000000000000004
0.3
false

0.3
true
0.3
true

Approximately compare floating point numbers

  • TODO

  • approx_eq

  • float-cmp

  • Where ULP stands for "units of least precision", or "units in the last place".

use float_cmp::approx_eq;

fn main() {
    let x = 1.0;
    let y = 2.0;
    let z = 1.0;
    println!("{}", x < y);
    println!("{}", x == z);
    println!();

    let x = 0.1 + 0.2;
    let y = 0.3;
    println!("{}", x);
    println!("{}", y);
    println!("{}", x == y);
    println!("{}", approx_eq!(f32, x, y, ulps = 17));

    let x = 0.00001;
    let y = 0.000010000001;
    println!("{}", x == y);
    println!("{}", approx_eq!(f32, x, y, ulps = 20));
}
true
true

0.3
0.3
true
true
false
true

NaN - Not a Number

  • NaN

  • sqrt

  • Floating point numbers, f32 or f64, can also represent a value called NaN or Not a Number.

  • One way to get the number is to take the square root of -1.0.

  • Most operations with a NaN result in NaN.

  • Two NaN values are not equal.

  • The sqrt (square root) method is not implemented for integers.

fn main() {
    let number: f32 = -1.0;
    let i = number.sqrt();
    println!("{}", i);
    let j = number.sqrt();

    println!("{}", i + 10.0);
    println!("{}", i == j);
}
NaN
NaN
false

Infinite floating point numbers

  • inf

  • NaN

  • You get inf or -inf if you devide by 0.0 or -0.0 respectively.

  • Adding inf to -inf yields a NaN.

  • Integers don't have infinite values.

fn main() {
    let infinite = 1.0 / 0.0;
    println!("{}", infinite);

    let negative_infinite = 1.0 / -0.0;
    println!("{}", negative_infinite);

    let what = infinite + negative_infinite;
    println!("{}", what);

    // Integers don't handle infinite
    // let infinite = 1/0;
    // ^^^ attempt to divide `1_i32` by zero
    //println!("{}", infinite);
}
inf
-inf
NaN

Complex numbers

  • TODO

  • The num-complex seems to be the most popular one.

Functions and test adding numbers

fn main() {
    let res = add(19, 23);
    println!("Result: {res}");
}

fn add(a: u32, b: u32) -> u32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);

        let result = add(19, 23);
        assert_eq!(result, 42);
    }
}
cargo run
cargo test

Exercise: Rectangle ARGS with protection

  • Change the previous solution to have a function that accepts 2 u8 values and returns the area as u8.
  • What should happen if the two numbers are both 20?
  • One solution should return 255 in that case.
  • You might provide other solutions as well, but for now this one is enough.

Exercise: Rectangle add tests

  • Take the rectangle solution.
  • Move the "business logic" (the computation) to one or more functions.
  • Write tests to verify the functions.

Exercise: Circle add tests

  • Take the circle solution.
  • Move the "business logic" (the computation) to one or more functions.
  • Write tests to verify the functions.

Solution: Rectangle ARGS with protection

fn main() {
    let res = area(2, 3);
    assert_eq!(res, 6);
}

#[allow(clippy::let_and_return)]
fn area(width: u8, length: u8) -> u8 {
    let area = width.saturating_mul(length);
    area
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_area() {
        assert_eq!(area(2, 3), 6);
        assert_eq!(area(20, 20), 255);
    }
}

Constants

Rust constants

  • const

  • Value that can be computed during compilation.

  • Cannot be changed.

  • Must have type declaration.

  • Name must be UPPER_CASE.

#[allow(clippy::approx_constant)]
fn main() {
    const NAME: &str = "Foo";
    println!("{}", NAME);

    const PI: f64 = 3.14;
    println!("{}", PI);
    // PI = 3.1; // cannot assign to this expression

    const DAY: i32 = 60 * 60 * 24;
    println!("{}", DAY);
}
Foo
3.14
86400

Constant shadowing

  • const
fn main() {
    const NAME: &str = "Foo";
    println!("name={NAME}");

    {
        println!("name={NAME}");
        const NAME: &str = "Bar";
        println!("name={NAME}");
    }
    println!("name={NAME}");
}
name=Foo
name=Bar
name=Bar
name=Foo

Characters

Single character

fn main() {
    let chars = ['1', 'a', 'א', 'Ω', '😇', '😈'];
    for ch in chars {
        let usize_num = ch as usize;
        let u32_num = ch as u32;

        let back = char::from_u32(u32_num).expect("Could not convert to char");

        println!(
            "{ch}\t{usize_num:6} {u32_num:6} {back} \t{} {}",
            (ch == back),
            ch.len_utf8()
        );
    }
}
1	    49     49 1 	true 1
a	    97     97 a 	true 1
א	  1488   1488 א 	true 2
Ω	   937    937 Ω 	true 2
😇	128519 128519 😇 	true 4
😈	128520 128520 😈 	true 4

Random

Using 3rd party libraries

Random module

cargo new show-random
cd show-random
cargo add random
[package]
name = "show-random"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.8.5"
fn main() {
    {
        let random_bool: bool = rand::random();
        println!("random_bool: {random_bool}");

        let random_i8: i8 = rand::random();
        println!("random_i8: {random_i8}");

        let random_f32: f32 = rand::random();
        println!("random_f32: {random_f32}");
    }

    {
        use rand::Rng;
        let random_number = rand::thread_rng().gen_range(1..=100);
        println!("random_number: {random_number}");
    }
}
cargo run
random_bool: true
random_i8: 69
random_f32: 0.59957224
random_number: 59

STDIN

Rust - read input from STDIN

  • std::io

  • String

  • stdin

  • read_line

  • expect

  • Module std::io

  • String::new() - Creates a new mutable empty string

  • &mut name passes a reference of the variable to the function as mutable variable. & indicates it is a reference.

  • read_line reads a line from the command line

use std::io;

fn main() {
    let mut name = String::new();

    println!("Please type in your name:");
    io::stdin()
        .read_line(&mut name)
        .expect("Failed to get input");

    println!("Hello {}, how are you?", name);
}
Please type in your name:
Foo Bar
Hello Foo Bar
, how are you?

Two problems:

  • The response looks broken. It has a newline after the name.
  • After the prompt the program waits on a new line.

Rust - read STDIN - remove trailing newline (trim, chomp)

  • trim_end

  • to_owned

  • trim_end removes trailing whitespace.

  • to_owned Converts the &str to String to be able to assign to the name variable again.

use std::io;

fn main() {
    let mut name = String::new();

    println!("Please type in your name:");
    io::stdin()
        .read_line(&mut name)
        .expect("Failed to get input");

    name = name.trim_end().to_owned();

    println!("Hello {name}, how are you?");
}

Rust - flush STDOUT - read STDIN

  • print!

  • Write

  • std::io::Write

  • We use print! and not println!

  • We use the std::io::Write trait that includes the flush method.

use std::io;
use std::io::Write;

fn main() {
    let mut name = String::new();

    print!("Please type in your name: ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut name)
        .expect("Failed to get input");
    name = name.trim_end().to_owned();

    println!("Hello {name}, how are you?");
}

Get number from STDIN

use std::io;
use std::io::Write;

fn main() {
    let mut height_str = String::new();

    print!("Please type in your height in cm: ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut height_str)
        .expect("Failed to get input");

    let height: i32 = height_str.trim().parse().expect("Could not convert to i32");

    println!("Your height is 1 cm less than {}", height + 1);
}

Get number from STDIN - same variable

use std::io;
use std::io::Write;

fn main() {
    let mut height = String::new();

    print!("Please type in your height in cm: ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut height)
        .expect("Failed to get input");

    let height: i32 = height.trim().parse().expect("Could not convert to i32");

    println!("Your height is 1 cm less than {}", height + 1);
}

Get number (i32) in using a function

  • We have not learned functions yet, but in order to make it easier to copy paste this example later we have a solution here
use std::io;
use std::io::Write;

fn main() {
    let x = get_number();
    println!("{x}");
}

fn get_number() -> i32 {
    let mut number = String::new();

    print!("Please type in an integer: ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut number)
        .expect("Failed to get input");

    let number: i32 = number.trim().parse().expect("Could not convert to i32");

    number
}

Exercise: STDIN rectangle

  • Ask the user for the length and the width of a rectangle and print the area and the circumference to the screen.

Exercise: STDIN circle

  • Ask the user for the radius of a circle and print the area and the circumference to the screen.

Exercise: STDIN calculator

  • The program will ask the user for a number, and for an operator (+, -, /, *) and for another number.
  • It will then do the appropriate mathematical operation and print the result.

Solution STDIN rectangle

use std::io;
use std::io::Write;

fn main() {
    let mut length = String::new();
    let mut width = String::new();

    print!("Length: ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut length)
        .expect("Failed to get input");

    let length: f32 = length.trim().parse().expect("Could not convert to f32");

    print!("Width: ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut width)
        .expect("Failed to get input");

    let width: f32 = width.trim().parse().expect("Could not convert to f32");

    println!(
        "Length: {} Width: {}, Area: {} Circumference: {}",
        length,
        width,
        length * width,
        2.0 * (length + width)
    );
}

Solution: STDIN calculator

use std::io;
use std::io::Write;

fn main() {
    let mut var_a = String::new();
    let mut op = String::new();
    let mut var_b = String::new();

    print!("a: ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut var_a)
        .expect("Failed to get input");

    let var_a: f64 = var_a.trim().parse().expect("Could not convert to f64");

    print!("op: ");
    io::stdout().flush().expect("Oups");
    io::stdin().read_line(&mut op).expect("Failed to get input");
    op = op.trim().to_owned();

    print!("b: ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut var_b)
        .expect("Failed to get input");

    let var_b: f64 = var_b.trim().parse().expect("Could not convert to f64");

    let res: f64;
    if op == "+" {
        res = var_a + var_b;
    } else if op == "-" {
        res = var_a - var_b;
    } else if op == "*" {
        res = var_a * var_b;
    } else if op == "/" {
        res = var_a / var_b;
    } else {
        eprintln!("Unrecognized operator '{op}'");
        std::process::exit(1);
    }

    println!("{var_a} {op} {var_b} = {res}");
}

Loops in Rust

Three types of loops in Rust

  • while
  • loop
  • for

Two loop controls

  • break
  • continue

While loop in Rust

  • while

  • We usually use a while loop if we don't know how many iterations we'll have to do.

use std::io;
use std::io::Write;

fn main() {
    let mut total = 0;
    while total < 10 {
        let number = get_number();
        total += number;
        println!("total {}", total);
    }
}

fn get_number() -> i32 {
    let mut number = String::new();

    print!("Please type in an integer: ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut number)
        .expect("Failed to get input");

    let number: i32 = number.trim().parse().expect("Could not convert to i32");

    number
}

Rust: loop with break

  • loop

  • break

  • If we cannot have the condition at the top of the loop, in some languages we write while true. In Rust we use loop.

  • We better have a condition with a break or we create an infinite loop!

use std::io;
use std::io::Write;

fn main() {
    loop {
        let number = get_number();
        println!("number {}", number);
        if number == 42 {
            break;
        }
    }
}

fn get_number() -> i32 {
    let mut number = String::new();

    print!("Please type in an integer: ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut number)
        .expect("Failed to get input");

    let number: i32 = number.trim().parse().expect("Could not convert to i32");

    number
}

break returns value from loop

  • loop
  • break
use rand::Rng;
fn main() {
    let mut number = 0;
    let last_number = loop {
        let random_number = rand::thread_rng().gen_range(1..=20);
        number += random_number;
        println!("{number}");
        if number > 100 {
            break random_number;
        }
    };

    println!();
    println!("{number} {last_number}");
}

for loop in Rust

  • for

  • range

  • If we want to iterate over a set of elements, eg. a range of numbers, then we ususally use for.

  • 1..5 means the right-hand limit is NOT included.

  • 1..=5 means the right-hand limit is included

fn main() {
    for ix in 1..5 {
        println!("count {}", ix);
    }
    println!();
    for ix in 1..=5 {
        println!("count {}", ix);
    }
}
count 1
count 2
count 3
count 4

count 1
count 2
count 3
count 4
count 5

Conditional operation and boolean values in Rust

Conditional: if

  • if
fn main() {
    let x = 23;
    let y = 32;
    if x < y {
        println!("x is smaller than y");
    }
}
x is smaller than y

Conditional: if - else

  • if
  • else
fn main() {
    let x = 23;
    let y = 32;
    if x > y {
        println!("x is bigger than y");
    } else {
        println!("x is smaller than y or equal to it");
    }
}
x is smaller than y or equal to it

Conditional: else - if

  • arms

  • else if

  • elseif

  • elsif

  • The code pathes in an if-else-if statement are called "arms".

  • comparison_chain

#[allow(clippy::comparison_chain)]
fn main() {
    let x = 23;
    let y = 32;
    if x > y {
        println!("x is bigger than y");
    } else if x < y {
        println!("x is smaller than y");
    } else {
        println!("x is equal to y");
    }
}
x is smaller than y

Avoid the comparison chain using match

  • match
use std::cmp::Ordering;
fn main() {
    let x = 23;
    let y = 32;
    match x.cmp(&y) {
        Ordering::Greater => println!("x is bigger than y"),
        Ordering::Less => println!("x is smaller than y"),
        Ordering::Equal => println!("x is equal to y"),
    }

    println!(
        "{}",
        match x.cmp(&y) {
            Ordering::Greater => "x is bigger than y",
            Ordering::Less => "x is smaller than y",
            Ordering::Equal => "x is equal to y",
        }
    )
}

Rust: boolean values true and false

  • true
  • false
fn main() {
    let x = true;
    let y = false;

    println!("x is {x}");
    println!("y is {y}");

    if x {
        println!("x in if part");
    } else {
        println!("x in else part");
    }

    if y {
        println!("y in if part");
    } else {
        println!("y in else part");
    }
}
x is true
y is false
x in if part
y in else part

Assign result of conditional to variable

fn main() {
    let x = 3;
    let y = 4;

    let z = x < y;
    println!("{}", z);

    let z = x > y;
    println!("{}", z);
}
true
false

Rust: other types don't have true/false values

fn main() {
    let x = 3;
    if x {
        println!("x is true");
    }
}
error[E0308]: mismatched types
 --> examples/booleans/other.rs:3:8
  |
3 |     if x {      // expected `bool`, found integer
  |        ^ expected `bool`, found integer

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
  • expected bool, found integer

Toggle

  • not

  • ! is the not-operator

fn main() {
    let mut t = true;
    println!("{t}");

    t = !t;
    println!("{t}");

    t = !t;
    println!("{t}");
}
true
false
true

if-else returns a value

  • This expersssion must have an else part!
  • The last statement in both the if and the else part has no ; at the end and thus they are called expressions and not statements.
fn main() {
    let x = 1;
    let y = 2;

    let z = if x < y { x + y } else { x * y };

    println!("{z}");
}

Conditional (Ternary) operator

  • ?:
  • if else
fn main() {
    let b = true;
    let answer = if b { 1 } else { 2 };
    println!("{answer}");

    let b = false;
    let answer = if b { 1 } else { 2 };
    println!("{answer}");
}
1
2

match

  • match

  • case

  • Similar to case or switch in other languages, match provides several arms.

fn greet(text: &str) {
    match text {
        "morning" => println!("Good morning"),
        "night" => println!("Goodnight"),
        // "morning" => println!("Again"),  warning: unreachable pattern
        "Jane" | "Joe" => println!("Hello {text}"),
        _ => println!("Hello World!"),
    }
}

fn main() {
    greet("morning");
    greet("night");
    greet("Jane");
    greet("Joe");
    greet("George");
}
Good morning
Goodnight
Hello Jane
Hello Joe
Hello World!

match all the numbers of an integer type

fn main() {
    let n = get_arg().parse::<u8>().unwrap();
    match n {
        0 => println!("zero"),
        1..=20 => println!("small"),
        21..=255 => println!("big"),
        //        21..= => println!("big"),
        //        _ => println!("other"),

        // 21..=254 => println!("big"),
        // non-exhaustive patterns: `u8::MAX` not covered
        // pattern `u8::MAX` not covered
    }
}

fn get_arg() -> String {
    let args = std::env::args().collect::<Vec<_>>();
    if args.len() != 2 {
        eprintln!("{} NUMBER", args[0]);
        std::process::exit(1);
    }

    args[1].to_owned()
}

match all the numbers of a float type

fn main() {
    let n = get_arg().parse::<f32>().unwrap();
    match n {
        0.0 => println!("zero"),
        1.0..=20.0 => println!("small"),
        21.0.. => println!("big"),
        _ => println!("other"),
    }
}

fn get_arg() -> String {
    let args = std::env::args().collect::<Vec<_>>();
    if args.len() != 2 {
        eprintln!("{} NUMBER", args[0]);
        std::process::exit(1);
    }

    args[1].to_owned()
}

match with conditions

  • The variable in each arm can be all the same or they can be different.
fn number(num: i32) {
    match num {
        x if x > 30 => println!("{} is above 30", x),
        x if x > 20 => println!("{} is above 20", x),
        neg if neg < 0 => println!("{} is negative", neg),
        _ => println!("other: {num}"),
    }
}

fn main() {
    number(40);
    number(30);
    number(0);
    number(-7);
}
40 is above 30
30 is above 20
other: 0
-7 is negative

Exercise: one-dimensional space-fight

  • We develop an interactive game called the one-dimensional space-fight.

  • We have a spaceship in the one-dimensional space we need to shoot down.

  • The distance of the spaceship is represented by an integer number. We can shoot by entering an integer number.

  • The computer generates a random integer number between 0-20. (The distance of the spaceship.)

  • The player shoots by entering a number.

  • The computer tells us if our shot was too short, too long or if we hit the target.

  • If we hit the target the game is over. We are told how many shots did we fire. We are asked if we would like to play again or quit.

  • During the game we can press "c" (that stands for cheat) and the computer will reeal the distance of the spaceship.

  • We can also press "q" and we will quite the game.

  • We can also press "n" and we start a new game, giving up on the current fight.

Solution: one-dimensional space-fight

{% embed include file="src/examples/number-guessing-game/number-guessing-game/Cargo.toml)

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Number Guessing game");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Input your guess: ");

        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
        guess = guess.trim().to_owned();

        if guess == "c" {
            println!("The secret number is {secret_number}");
            continue;
        }
        if guess == "x" || guess == "q" {
            println!("Quitter!");
            break;
        }

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

        let guess: u32 = match guess.parse() {
            Ok(num) => num,
            Err(err) => {
                println!("Error: {err}");
                continue;
            }
        };

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

Arrays in Rust

Arrays in Rust

  • Array
  • The Array Type in the book.
  • An Array in Rust has a fixed number of elements.
  • All the elements are of the same type.
  • In mutable arrays we can change the content, but we still cannot add or remove elements.

Rust array of numbers, length of array

  • len

  • Create array

  • Get the number of elements, the length of the array using len.

  • Access elements in the array using square bracket indexing.

fn main() {
    let numbers: [i8; 3] = [10, 11, 12];

    println!("{:?}", numbers);
    println!("{}", numbers.len());
    println!("{}", numbers[0]);
}
[10, 11, 12]
3
10

Array of strings - access element by index

  • mut

  • To allow us to change the values in the array we have to make it mutable using mut.

  • In most cases there is no need to decalre the type of the values and the length. Rust can infer.

fn main() {
    let mut animals = ["cat", "snake", "fish"];
    println!("{animals:?}");

    println!("{}", animals[1]);
    animals[1] = "crab";

    println!("{animals:?}");
}
["cat", "snake", "fish"]
snake
["cat", "crab", "fish"]

Array of structs

  • Debug

  • dead_code

  • We can also create an array from structs.

#[derive(Debug)]
#[allow(dead_code)]
struct Person {
    fname: String,
    lname: String,
}

fn main() {
    let mut people = [
        Person {
            fname: String::from("John"),
            lname: String::from("Lennon"),
        },
        Person {
            fname: String::from("Paul"),
            lname: String::from("McCartney"),
        },
    ];

    println!("{:#?}", people);

    people[0].fname = String::from("Jane");
    println!("{:#?}", people);
}
[
    Person {
        fname: "John",
        lname: "Lennon",
    },
    Person {
        fname: "Paul",
        lname: "McCartney",
    },
]
[
    Person {
        fname: "Jane",
        lname: "Lennon",
    },
    Person {
        fname: "Paul",
        lname: "McCartney",
    },
]

Array iterate on elements

fn main() {
    let numbers: [i32; 3] = [10, 11, 12];

    for num in numbers {
        println!("{}", num);
    }
}
10
11
12

Rust array iterate over idices

#[allow(clippy::needless_range_loop)]
fn main() {
    let numbers: [i32; 3] = [10, 11, 12];

    for ix in 0..numbers.len() {
        println!("{} {}", ix, numbers[ix]);
    }
}
0 10
1 11
2 12

Rust array iterate indices and elements with enumerate

  • iter
  • enumerate
fn main() {
    let numbers: [i32; 3] = [10, 11, 12];

    for (ix, number) in numbers.iter().enumerate() {
        println!("{ix} {}", number);
    }
}
0 10
1 11
2 12

Rust arrays are not mutable

  • By default arrays are not mutable and thus we cannot change a value.
  • This example has a compilation error.
fn main() {
    let numbers: [i32; 3] = [10, 11, 12];

    println!("{:?}", numbers);

    numbers[0] = 30; // cannot assign to `numbers[_]`, as `numbers` is not declared as mutable
    println!("{:?}", numbers);
}

Make the Rust array mutable

  • mut
fn main() {
    let mut numbers: [i32; 3] = [10, 11, 12];

    println!("{:?}", numbers);

    numbers[0] = 30;
    println!("{:?}", numbers);
}

Creating an array with the same value

fn main() {
    let counter = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
    println!("{} {:?}", counter.len(), counter);

    let counter = [0; 10];
    println!("{} {:?}", counter.len(), counter);

    let answer = [42; 4];
    println!("{} {:?}", answer.len(), answer);
}
10 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
10 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
4 [42, 42, 42, 42]

Exercise: Count digits

  • Given an array of digits, count how many times each digit appears.
let digits = [1, 2, 1, 3, 4, 5, 6, 7, 8, 8, 4, 3, 9, 7, 8, 5, 2, 3, 3, 2, 2, 4, 2, 4, 2, 1];
0: 0
1: 3
2: 6
3: 4
4: 4
5: 2
6: 1
7: 2
8: 3
9: 1

Solution: Count digits

fn main() {
    let digits = [
        1, 2, 1, 3, 4, 5, 6, 7, 8, 8, 4, 3, 9, 7, 8, 5, 2, 3, 3, 2, 2, 4, 2, 4, 2, 1,
    ];
    let mut counter = [0i8; 10];
    for ch in digits {
        counter[ch] += 1;
    }
    for (ix, count) in counter.iter().enumerate() {
        println!("{ix}: {}", count);
    }
}
0: 0
1: 3
2: 6
3: 4
4: 4
5: 2
6: 1
7: 2
8: 3
9: 1

Option

The Option enum

The Option enum.

#![allow(unused)]
fn main() {
enum Option<T> {
    None,
    Some(T),
}
}

Create Option

  • None
  • Some
  • Option
fn main() {
    //let x = None;

    for value in [None, Some(42)] {
        println!("{value:?}");

        match value {
            None => println!("It was None"),
            Some(number) => println!("It was this: {number} number"),
        };
    }

    println!();

    for value in [None, Some(3.1)] {
        println!("{value:?}");

        match value {
            None => println!("It was None"),
            Some(number) => println!("It was this: {number} number"),
        };
    }
}

Checked rectangle

#![allow(dead_code)]
#![allow(clippy::manual_unwrap_or_default)]
#![allow(clippy::manual_unwrap_or)]

fn main() {
    nulled_area(20, 30);
}

fn nulled_area(width: u8, length: u8) -> u8 {
    let area_option = width.checked_mul(length);
    //println!("{:?}", area_option);
    match area_option {
        Some(area) => area,
        None => 0,
    }
}

fn checked_area(width: u8, length: u8) -> Option<u8> {
    width.checked_mul(length)
    //    let area_option = width.checked_mul(length);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_nulled_area() {
        assert_eq!(nulled_area(3, 4), 12);
        assert_eq!(nulled_area(20, 20), 0);
    }

    #[test]
    fn test_checked_area() {
        assert_eq!(checked_area(3, 4), Some(12));
        assert_eq!(checked_area(20, 20), None);
    }
}

Get language function returning Option

  • Option
  • match
#![allow(dead_code)]
fn main() {
    for ext in ["pl", "c"] {
        let lang = get_language(ext);
        println!("{:?}", lang);
        match lang {
            None => (),
            Some(val) => println!("{}", val),
        };

        if let Some(val) = lang {
            println!("if: {}", val);
        } else {
            println!("we had none");
        }
    }
}

fn get_language_empty_string(ext: &str) -> &str {
    if ext == "rs" {
        return "rust";
    }
    if ext == "py" {
        return "python";
    }
    if ext == "pl" {
        return "perl";
    }

    ""
}

fn get_language_empty_string_match(ext: &str) -> &str {
    match ext {
        "rs" => "rust",
        "py" => "python",
        "pl" => "perl",
        _ => "",
    }
}

fn get_language(ext: &str) -> Option<&str> {
    match ext {
        "rs" => Some("rust"),
        "py" => Some("python"),
        "pl" => Some("perl"),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_language_empty_string() {
        assert_eq!(get_language_empty_string("rs"), "rust");
        assert_eq!(get_language_empty_string("py"), "python");
        assert_eq!(get_language_empty_string("pl"), "perl");
        assert_eq!(get_language_empty_string("qqrq"), "");
    }

    #[test]
    fn test_get_language_empty_string_match() {
        assert_eq!(get_language_empty_string_match("rs"), "rust");
        assert_eq!(get_language_empty_string_match("py"), "python");
        assert_eq!(get_language_empty_string_match("pl"), "perl");
        assert_eq!(get_language_empty_string_match("qqrq"), "");
    }

    #[test]
    fn test_get_language() {
        assert_eq!(get_language("rs"), Some("rust"));
        assert_eq!(get_language("py"), Some("python"));
        assert_eq!(get_language("pl"), Some("perl"));
        assert_eq!(get_language("qqrq"), None);
    }
}

Strings in Rust

Create a String

  • println!

  • String

  • from

  • to_string

  • to_owned

  • The first one is a reference to a string embedded in the program. We can only chnage this in the editor. Not while the program is running.

  • String::from can create a "real" string from an embedded string. This is also what we get from any input. (STDIN, ARGS, file, network etc.)

  • to_string is the method to stringify any value. It works here as well, but we can be clearer with to_owned.

  • to_owned convert a reference to an owned string. More about ownership later.

fn main() {
    let text_1 = "Hello World!";
    println!("{text_1}");

    let text_2 = String::from("Hello World!");
    println!("{text_2}");

    let text_3 = "Hello World!".to_string();
    println!("{text_3}");

    let text_4 = "Hello World!".to_owned();
    println!("{text_4}");

    assert_eq!(text_1, "Hello World!");
    assert_eq!(text_1, text_2);
}

{% embed include file="src/examples/strings/create/out.out)

Create empty string and grow it using push and push_str

  • String::new
  • push
  • push_str
fn main() {
    let mut text = String::new();
    println!("{text:?}");

    text.push_str("Hello");
    println!("{text:?}");

    text.push(' ');
    println!("{text:?}");

    text.push_str("World");
    println!("{text:?}");

    text.push('!');
    println!("{text:?}");
}
""
"Hello"
"Hello "
"Hello World"
"Hello World!"

Length of string

  • len

  • With the len method we can get the length of a string in bytes.

fn main() {
    let text = String::from("Hello World!");
    println!("{text}");

    let length = text.len();
    println!("{length}");

    let text = String::from("👻👽👾");
    println!("{text}");

    let length = text.len();
    println!("{length}");
}
Hello World!
12
👻👽👾
12

Capacity of string

  • len
  • capacity
  • macro_rules!
macro_rules! prt {
    ($var:expr) => {
        println!("{:2} {:2} {:?}", $var.len(), $var.capacity(), $var,);
    };
}

fn main() {
    let mut text = String::new();
    prt!(text);

    text.push('A');
    prt!(text);

    text.push_str(" black ");
    prt!(text);

    text.push_str("cat");
    prt!(text);

    text.push_str(" climebed");
    prt!(text);
}
 0  0 ""
 1  8 "A"
 8  8 "A black "
11 16 "A black cat"
20 32 "A black cat climebed"

Strings and memory allocation

  • len
  • capacity
  • macro_rules!
  • as_ptr
  • :p
macro_rules! prt {
    ($var:expr) => {
        println!(
            "{:2} {:2} {:p} {:15?} '{}'",
            $var.len(),
            $var.capacity(),
            &$var,
            $var.as_ptr(),
            $var
        );
    };
}
fn main() {
    let mut text = String::new();
    prt!(text);
    text.push('a');
    prt!(text);

    let name = String::from("foobar");
    prt!(name);

    text.push('b');
    prt!(text);
    text.push_str("123456");
    prt!(text);

    text.push('x');
    prt!(text);

    text.push_str("123456789123143274368741");
    prt!(text);
}
 0  0 0x7ffce1f9ce58             0x1 ''
 1  8 0x7ffce1f9ce58  0x655352d24b80 'a'
 6  6 0x7ffce1f9d640  0x655352d24ba0 'foobar'
 2  8 0x7ffce1f9ce58  0x655352d24b80 'ab'
 8  8 0x7ffce1f9ce58  0x655352d24b80 'ab123456'
 9 16 0x7ffce1f9ce58  0x655352d24b80 'ab123456x'
33 33 0x7ffce1f9ce58  0x655352d24bc0 'ab123456x123456789123143274368741'

Rust - string ends with

  • ends_with
fn main() {
    let names = [String::from("name.txt"), String::from("other")];
    for name in names {
        print!("name: '{name}'");
        if name.ends_with(".txt") {
            print!(" ends with .txt");
        }
        println!();
    }
}
name: 'name.txt' ends with .txt
name: 'other'

Rust - string starts with

  • starts_with
fn main() {
    let names = [
        String::from("name.txt"),
        String::from("other"),
        String::from("narnia"),
    ];
    for name in names {
        print!("name: '{name}'");
        if name.starts_with("na") {
            print!(" starts with na");
        }
        println!();
    }
}
name: 'name.txt' starts with na
name: 'other'
name: 'narnia' starts with na

To lower, to upper case

  • to_lowercase
  • to_uppercase
fn main() {
    let text = String::from("The Green cat");
    println!("{}", text);
    println!("{}", text.to_lowercase());
    println!("{}", text.to_uppercase());
}
The Green cat
the green cat
THE GREEN CAT

Accessing characters in a string

#![allow(unused)]
fn main() {
text[3]
}
  • Is this the 4th byte or the 4th unicode character?
  • The former would be garbage if we have anything else besides ASCII in the string.
  • The latter would mean accessing elements is not a O(1) operation as we would have to iterate over all the previous characters.

Rust - string slices

  • Provide address of bytes, but make sure you are on the character boundaries!
fn main() {
    let text = String::from("The black cat: 🐈‍ climbed the green tree: 🌳!");
    println!("{}", text);
    println!("'{}'", &text[4..4]); // ''  empty string
    println!("'{}'", &text[4..=4]); // 'b'
    println!("'{}'", &text[4..9]); // 'black'
    println!("'{}'", &text[30..]); // ' the green tree: 🌳!'
    println!("'{}'", &text[..4]); // 'The '

    println!("'{}'", &text[15..22]); // '🐈‍'

    //println!("'{}'", &text[16..22]); // panic!: byte index 16 is not a char boundary; it is inside '🐈' (bytes 15..19)

    //println!("'{}'", &text[25..60]);  // panic! at 'byte index 60 is out of bounds
}
The black cat: 🐈‍ climbed the green tree: 🌳!
''
'b'
'black'
' the green tree: 🌳!'
'The '
'🐈‍'

Rust - string characters

  • chars
  • Some
  • None
#[allow(clippy::iter_skip_next)]
fn main() {
    let text = String::from("The black cat");

    println!("{}", text);
    println!("{:?}", text.chars());
    println!("{:?}", text.chars().nth(4));
    //println!("{:?}", text.chars().skip(4).nth(0));
    println!("{:?}", text.chars().skip(4).next()); // clippy tells us to use `.nth(4)`
    println!("{:?}", text.chars().nth(20));

    println!("--------");

    for n in [-1, 4, 20] {
        let char = text.chars().nth(n as usize);
        println!("{:?}", char);
        match char {
            Some(value) => println!("{}", value),
            None => println!("This was None"),
        }
    }
}
The black cat
Chars(['T', 'h', 'e', ' ', 'b', 'l', 'a', 'c', 'k', ' ', 'c', 'a', 't'])
Some('b')
Some('b')
None
--------
None
This was None
Some('b')
b
None
This was None

Iterate over the characters of a string

  • chars
fn main() {
    let text = String::from("The black cat");
    println!("{text}");
    println!("{:?}", text.chars());

    for ch in text.chars() {
        println!("{ch}");
    }
}
The black cat
Chars(['T', 'h', 'e', ' ', 'b', 'l', 'a', 'c', 'k', ' ', 'c', 'a', 't'])
T
h
e
 
b
l
a
c
k
 
c
a
t

Rust - reverse string

  • chars
  • rev
  • collect

This is a simple, and apparently partially incorrect solution. There is a crate called unicode_reverse for doing it right.

fn main() {
    let texts = [
        String::from("Hello"),
        String::from("Abc"),
        String::from("שלום"),
    ];
    for text in texts {
        let reversed = reverse(&text);
        let original = reverse(&reversed);
        println!("{text:6} - {reversed:6} - {original:6}");
    }
}

fn reverse(text: &str) -> String {
    let reversed: String = text.chars().rev().collect();
    reversed
}
Hello  - olleH  - Hello 
Abc    - cbA    - Abc   
שלום   - םולש   - שלום  

Concatenation str with str

  • In the example we have two strings hard-coded in the binary of our executable.
fn main() {
    let str1 = "Hello";
    let str2 = "World";

    // let text = str1 + str2; // cannot add `&str` to `&str`
    let text = str1.to_owned() + " " + str2;
    println!("{}", text);
}
Hello World

Concatenation String with String

fn main() {
    let string1 = String::from("Apple");
    let string2 = String::from("Banana");

    //let text = string1 + string2; // mismatched types
    let text = string1 + &string2; // we move strin1 and then copy the content of string2
    println!("{}", text);
    println!("{}", string2);
    // println!("{}", string1);   // error[E0382]: borrow of moved value: `string1`
}
AppleBanana
Banana

Concatenation String with String (clone)

  • clone
fn main() {
    let string1 = String::from("Apple");
    let string2 = String::from("Banana");

    let text = string1.clone() + &string2; // we move strin1 and then copy the content of string2
    println!("{}", text);
    println!("{}", string2);
    println!("{}", string1);
}
AppleBanana
Banana
Apple

Concatenation String with str

macro_rules! prt {
    ($var:expr) => {
        println!(
            "{:2} {:2} {:p} {:15?} '{}'",
            $var.len(),
            $var.capacity(),
            &$var,
            $var.as_ptr(),
            $var
        );
    };
}

fn main() {
    let str1 = "Hello";
    let string1 = String::from("Apple");
    prt!(string1);

    //let text = str1 + string1; // cannot add `String` to `&str`
    let text = string1 + str1;
    prt!(text);
    println!("{text}");
    //println!("{string1}");
}
AppleHello

Concatenate strings using format!

  • format!

  • In this case all the strings are copied so it is less efficient than where we move the string, but this means we can continue to use the original variables.

fn main() {
    let string1 = String::from("Apple");
    let string2 = String::from("Banana");
    let str3 = "Peach";

    let text = format!("{}-{}-{}-{}", string1, string2, str3, "other");
    println!("{}", text);
    println!("{}", string1);
    println!("{}", string2);
    println!("{}", str3);
}
Apple-Banana-Peach-other
Apple
Banana
Peach

concat

  • concat
fn main() {
    let string1 = String::from("Apple");
    let string2 = String::from("Banana");
    let str3 = "Peach";

    let other = [string1, string2, str3.to_owned()].concat();
    println!("{other}");
}
AppleBananaPeach

Split string

  • split
fn main() {
    let text = "mouse cat cat   oliphant";
    println!("{text}");
    let parts = text.split(' ');
    // println!("{:?}", parts);
    for part in parts {
        println!("{}", part);
    }
}
mouse cat cat   oliphant
mouse
cat
cat


oliphant

Split string on whitespace

  • split_whitespace
fn main() {
    let text = "mouse cat cat   oliphant";
    let parts = text.split_whitespace();
    // println!("{:?}", parts);
    for part in parts {
        println!("{}", part);
    }
}
mouse
cat
cat
oliphant

Split on newlines - use lines

  • lines

  • Lines

  • The lines method returns a Lines struct, an iterator over the lines.

fn main() {
    let text = "row 1\nrow 2\nrow 3\n";
    println!("{text}");
    println!("-----");

    let lines = text.lines();
    for line in lines {
        println!("line: {line}");
    }
}
row 1
row 2
row 3

-----
line: row 1
line: row 2
line: row 3

Append to string with push_str

  • push
  • push_str
  • to_string
#![allow(clippy::single_char_add_str)]

fn main() {
    let mut text = String::from("");
    println!("{text}");

    text.push('a');
    println!("{text}");

    text.push_str("bcd");
    println!("{text}");

    let literal_string = "x";
    text.push_str(literal_string);
    println!("{text}");

    let string = String::from("yz");
    text.push_str(&string);
    println!("{text}");

    let cr = 'e';
    text.push_str(&cr.to_string());
    println!("{text}");
}

a
abcd
abcdx
abcdxyz
abcdxyze

Create String from literal string using to_string

  • to_string

  • ToString is a trait that can convert anything to a String.

fn main() {
    let mut text = "abc".to_string();
    println!("{text}");

    text.push_str("efg");
    println!("{text}");
}
abc
abcefg

Str and String equality

fn main() {
    let a = "hello";
    let b = "hello";
    let c = String::from("hello");
    let d = String::from("hello");

    println!("{}", a == b);
    println!("{}", a == c);
    println!("{}", c == d);
}
true
true
true

String notes

  • str - addr, length (a view into a utf-8 encoded bytes located either in the binary, on the stack, or on the heap)
  • &str - borrowed str
  • String - addr, length, capacity (owner)

String replace all

  • replace
fn main() {
    let text = String::from("Black cat, brown cat, many cats and a dog");
    println!("{text}");

    let new = text.replace("cat", "mouse");
    println!("{new}");
    println!("{text}");
}
Black cat, brown cat, many cats and a dog
Black mouse, brown mouse, many mouses and a dog
Black cat, brown cat, many cats and a dog

String replace limited times

  • replacen
fn main() {
    let text = String::from("Black cat, brown cat, many cats and a dog");
    println!("{text}");

    let new = text.replacen("cat", "mouse", 2);
    println!("{new}");
    println!("{text}");
}
Black cat, brown cat, many cats and a dog
Black mouse, brown mouse, many cats and a dog
Black cat, brown cat, many cats and a dog

String replace range

  • replace_range
fn main() {
    let mut text = String::from("The black cat climbed the green tree");
    println!("{text}");

    text.replace_range(10..13, "dog");
    println!("{text}");

    text.replace_range(10..13, "elephant");
    println!("{text}");
}
The black cat climbed the green tree
The black dog climbed the green tree
The black elephant climbed the green tree

Function to combine two strings

fn main() {
    let fname = String::from("Foo");
    let lname = String::from("Bar");
    println!("{fname}");
    println!("{lname}");

    let res = combine(&fname, &lname);

    println!("Combined name {res}");
    println!("{fname}");
    println!("{lname}");
}

fn combine(fname: &str, lname: &str) -> String {
    //format!("{fname} {lname}")
    fname.to_owned() + " " + lname
}
Foo
Bar
Combined name Foo Bar
Foo
Bar

Ownership and strings

  • take ownership
fn main() {
    let name = String::from("Foo");
    println!("{name}");
    take_ownership(name);
    //println!("{name}"); // take_ownership moved the owner
}

fn take_ownership(name: String) {
    println!("in function: {name}");
}
Foo
in function: Foo
  • borrow
fn main() {
    let name = String::from("Foo");
    println!("{name}");
    borrow(&name);
    println!("{name}");
}

fn borrow(name: &str) {
    println!("in function: {name}");
}
  • give ownership
fn main() {
    let name = give_ownership();
    println!("{name}");
}

fn give_ownership() -> String {
    String::from("Foo")
}

Slice and change string

fn main() {
    let mut text = String::from("Foobar");
    println!("{}", text);

    let slice = &text[0..=2];
    println!("{}", slice);

    text.clear();
    text.push_str("qwerty");
    println!("{}", text);
    //println!("{}", slice);
}

Compare strings

  • cmp

  • Less

  • Equal

  • Greater

  • Ordering

  • We can use the regular <, >, == operators to compare both strings and string slices.

  • The cmp method returns a value from the Ordering enum.

fn main() {
    let x = "abc";
    let y = "abd";
    let z = "abd";
    println!("{}", x < y);
    println!("{:?}", x.cmp(y));
    println!("{}", y == z);
    println!("{:?}", y.cmp(y));
    println!();

    let x = String::from("abc");
    let y = String::from("abd");
    let z = String::from("abd");
    println!("{}", x < y);
    println!("{:?}", x.cmp(&y));
    println!("{}", y == z);
    println!("{:?}", y.cmp(&y));
}
true
Less
true
Equal

true
Less
true
Equal

Is one string in another string - contains?

  • contains

  • find

  • in

  • index

  • contains will return a boolean value telling if one string contains the other

  • find will return a number indicating the location of a substring (or None if the string cannot be found).

fn main() {
    let text = "The black cat climed the green tree";
    println!("{}", text.contains("cat"));
    println!("{}", text.contains("dog"));
    println!("{}", text.find("cat").unwrap());

    // println!("{}", text.find("dog").unwrap()); // panics

    println!("{}", text.find('a').unwrap());
    println!("{}", text[7..].find('a').unwrap());
}
true
false
10
6
4

Embed double quotes in string

  • String in Rust are inside double quotes.
  • It is easy to inlcude single quote in a string as it is not special.
  • In order to include a double quote we need to add the escape character, the back-slash, in-front of it.
  • Alternatively we can start the string using r#" and then we can end it with "#. This allows us to freely include double-quote in the string.
fn main() {
    let name = String::from("Foo");

    println!("Hello {name}, how are you?");
    println!("Hello '{name}', how are you?");
    println!("Hello \"{name}\", how are you?");
    println!(r#"Hello "{name}", how are you?"#);
}
Hello Foo, how are you?
Hello 'Foo', how are you?
Hello "Foo", how are you?
Hello "Foo", how are you?

Remove leading and trailing whitespace

  • trim

  • trim_end

  • trim_start

  • Read more about trim and the related functions.

fn main() {
    let text = String::from("  Some text  ");
    println!("original:   '{}'", text);
    println!("trim_end:   '{}'", text.trim_end());
    println!("trim_start: '{}'", text.trim_start());
    println!("trim:       '{}'", text.trim());
}
original:   '  Some text  '
trim_end:   '  Some text'
trim_start: 'Some text  '
trim:       'Some text'

Remove extra whitespace from string

  • split_whitespace

  • The second solution is obviously the better solution, but the first one might be applied to situations where we would like to get rid of other duplicate characters.

fn main() {
    let text = "   Some      white \n  \n    \n spaces   \ntext \t \n with tabs   \n";
    println!("'{}'", text);

    let short = text
        .replace(['\n', '\t'], " ")
        .trim()
        .split(' ')
        .filter(|short| !short.is_empty())
        .collect::<Vec<_>>()
        .join(" ");
    println!("'{}'", short);

    let other = text.split_whitespace().collect::<Vec<_>>().join(" ");
    println!("'{}'", other);
}
'   Some      white 
  
    
 spaces   
text 	 
 with tabs   
'
'Some white spaces text with tabs'
'Some white spaces text with tabs'

String is alphabetic or alphanumeric

  • is_alphabetic
  • is_alphanumeric
  • all
  • chars

The char type has methods such as is_alphabetic and is_alphanumeric and several other similar methods.

fn main() {
    let strings = vec!["text", "t xt", "t,xt", "😇😈", "Ωأㅏñ", "שלום"];

    for text in &strings {
        println!("{}: {}", text, text.chars().all(|chr| chr.is_alphabetic()));
    }
    println!();

    for text in &strings {
        println!("{}: {}", text, text.chars().all(char::is_alphabetic));
    }
    println!();

    for text in strings {
        println!(
            "{}: {}",
            text,
            text.chars().all(|chr| chr.is_alphabetic() || chr == ' ')
        );
    }
}
text: true
t xt: false
t,xt: false
😇😈: false
Ωأㅏñ: true
שלום: true

text: true
t xt: false
t,xt: false
😇😈: false
Ωأㅏñ: true
שלום: true

text: true
t xt: true
t,xt: false
😇😈: false
Ωأㅏñ: true
שלום: true

Compare memory address (pointer)

  • std::ptr::addr_of

  • addr_of

  • Another way to show that different pieces of strings are located in different places in the memory.

fn main() {
    let text_a = "Hello, world!";
    let text_b = "Hello, world!";
    println!("{}", text_a == text_b);
    println!(
        "{}",
        std::ptr::addr_of!(text_a) == std::ptr::addr_of!(text_b)
    );
    println!("{:?}", &std::ptr::addr_of!(text_a));
    println!("{:?}", &std::ptr::addr_of!(text_b));
    println!();

    let text_a = String::from("Hello, world!");
    let text_b = String::from("Hello, world!");
    println!("{}", text_a == text_b);
    println!(
        "{}",
        std::ptr::addr_of!(text_a) == std::ptr::addr_of!(text_b)
    );
    println!("{:?}", &std::ptr::addr_of!(text_a));
    println!("{:?}", &std::ptr::addr_of!(text_b));
}
true
false
0x7ffddbd50e38
0x7ffddbd50e50

Exercise: Count digits from string

  • Given a string of numbers, count how many times each digit appears.
let text = "1213 456 78843978523 3224 2421";
0: 0
1: 3
2: 6
3: 4
4: 4
5: 2
6: 1
7: 2
8: 3
9: 1

Exercise: concatenate strings

Write two programs.

  • Get two strings on the command line, concatenate them and print them.
  • Ask the user for two string on STDIN and concatenate them.

Exercise: is it a palindrome?

Exercise: is it an anagram?

  • Given two strings tell me if they are Anagrams?

Exercise: Get nth double character

Write a function that receives a string and returns the nth character that is duplicated.

"xyzaabb", 1   -> a
"xyzaabb", 2   -> b

Exercise: get first word

Given a sentence (letters and spaces), return the first word:

"The black cat" -> "The"
"example"       -> "example"

Solution: Count digits from string

fn main() {
    let text = "1213 456 78843978523 3224 2421";
    let mut counter = [0i8; 10];
    for ch in text.chars() {
        if ch == ' ' {
            continue;
        }
        let ix = ch as usize - '0' as usize;
        //println!("{ch}");
        //println!("{ix}");
        counter[ix] += 1;
    }
    for (ix, count) in counter.iter().enumerate() {
        println!("{ix}: {}", count);
    }
}
0: 0
1: 3
2: 6
3: 4
4: 4
5: 2
6: 1
7: 2
8: 3
9: 1

Command line arguments - ARGS

Command line arguments - print all args

  • std::env

  • args

  • Vec

  • We load the std::env module

  • #[allow(clippy::needless_range_loop)] is needed to silence clippy, the linter

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("My path is {}", args[0]);
    println!("Number of arguments is {}", args.len());

    #[allow(clippy::needless_range_loop)]
    for i in 1..args.len() {
        println!("Parameter {} is '{}'", i, args[i]);
    }
}
cargo run

My path is target/debug/args
Number of arguments is 1
cargo run apple banana

My path is target/debug/args
Number of arguments is 3
Parameter 1 is 'apple'
Parameter 2 is 'banana'

Command line program with a single file parameter

  • eprintln!
  • exit
  • std::process::exit
use std::env;
use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() != 2 {
        eprintln!("Usage: {} FILENAME", args[0]);
        process::exit(1);
    }

    let filename = &args[1];
    println!("We are working on file '{}'", filename);
}
cargo run

Usage: target/debug/some-tool FILENAME
cargo run data.csv

We are working on file 'data.csv'

Exercise: calucaltor args

Implement a calculator for the 4 basic operations (+, -, /, *) in Rust that accepts the parameters on the command line:

rustc calc.rs
./calc 3 + 4
7

./calc 3 - 4
-1
  • You should not use the rust.sh for this!
  • What's going on with multiplication?

Solution: calucaltor args

fn main() {
    let args: Vec<String> = std::env::args().collect();
    if args.len() != 4 {
        eprintln!("Usage {} a operator b", args[0]);
        eprintln!("Received {} parameters", args.len());
        std::process::exit(1);
    }
    let var_a: f64 = args[1].trim().parse().expect("Wanted a number");
    let op: &String = &args[2];
    let var_b: f64 = args[3].trim().parse().expect("Wanted a number");
    let res: f64;
    if op == "+" {
        res = var_a + var_b;
    } else if op == "-" {
        res = var_a - var_b;
    } else if op == "*" {
        res = var_a * var_b;
    } else if op == "/" {
        res = var_a / var_b;
    } else {
        eprintln!("Unrecognized operator '{op}'");
        std::process::exit(1);
    }

    println!("{var_a} {op} {var_b} = {res}");
}
cargo run 3 + 4

3 + 4 = 7
cargo run 3 '*' 4

3 * 4 = 12

Default value on the command line

  • In this example we set the config_file variable to the parameter passed on the command line
  • or to a default value that can be any string, including the empty string.
fn main() {
    let argv: Vec<String> = std::env::args().collect();

    let config_file = if argv.len() == 2 {
        &argv[1]
    } else {
        //""
        "config.yaml"
    };

    println!("'{}'", config_file);
}

Default path - return PathBuf

  • PathBuf

  • args

  • The user must supply the path to root and optionally the path to the pages.

  • If the user does not supply the path to the pages then we use root/pages

fn main() {
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 2 || args.len() > 3 {
        eprintln!("Usage: {} ROOT [PATH]", args[0]);
        std::process::exit(1);
    }
    let root = &args[1];
    let page = get_path(&args);
    println!("root: {}", root);
    println!("page: {:?}", page);
}

fn get_path(args: &[String]) -> std::path::PathBuf {
    if args.len() == 3 {
        return std::path::PathBuf::from(&args[2]);
    }
    let pb = std::path::PathBuf::from(&args[1]);
    pb.join("pages")
}

Returning the second parameter as the path to pages:

$ cargo run one two

root: one
page: "two"

Returning root/pages:

$ cargo run one
root: one
page: "one/pages"

Tuples in Rust

Rust tuple - fixed-sizes, mixed, ordered collection

  • tuple

  • parenthesis

  • ()

  • Each value can be any type. (heterogeneous)

  • The types and the number of elements are fixed at creation.

  • In this example the types are deducted from the values.

  • It is ordered.

  • We can access the individual elements using their index and the dot-notation.

  • The content can be changed only if the definition uses mut.

  • Tuple

  • Tuple Types

  • Examples

fn main() {
    let things = (11, '2', String::from("three"));
    println!("{}", things.0);
    println!("{}", things.1);
    println!("{}", things.2);
    println!("{:?}", things);
}
11
2
three
(11, '2', "three")

Define the types in the tuple

  • Optionally we can define the types of elements of a tuple.
  • This can be useful if we don't want the default types. (e.g the default integer type is i32, but here we use i8.)
fn main() {
    let things: (i8, char, String) = (11, '2', String::from("three"));
    println!("{:?}", things);
}
(11, '2', "three")

Change tuple (mutable)

  • mut

  • Use the mut keyword to make the values of the tuple mutable.

  • We still cannot add elements to the tuple. It's shape and the types of the fields are fixed.

  • Assign a new value to one of the elements using the dot-notation.

fn main() {
    let mut row = ("Blue", 300, 3.67);
    println!("{:?}", row);
    row.0 = "Green";

    row.1 = 99;

    println!("{:?}", row);
}
("Blue", 300, 3.67)
("Green", 99, 3.67)

Create tuple with types, but without values

  • mut

  • We can create a tuple without initializing. In this case it seems even more useful to declare the types. (Though not required.)

  • We must have the mut keyword to make it mutable.

  • Then later we can assign all the values at once.

  • Before we initialize all the values we cannot assign them one-by-one.

fn main() {
    let mut row: (&str, i32, f32);

    // row.0 = "Blue";
    // partially assigned binding `row` isn't fully initialized

    row = ("Purple", 300, 3.45);
    println!("{:?}", row);

    row = ("Green", 99, 4.1);
    println!("{:?}", row);
}
("Purple", 300, 3.45)
("Green", 99, 4.1)

Destructuring tuple

  • _

  • underscore

  • It is not destructing! Just looks similar.

  • It means taking the values of a tuple apart into individual variables.

  • We have to assign all the values.

  • We can use the underscore _ (multiple times) as a placeholder for the value we don't want to assign.

  • Alternatively, we could assign them one-by-one.

fn main() {
    let things = (11, '2', String::from("three"), 19, 23);

    let (a, _, c, _, e) = &things; // Destructuring the tuple
    println!("{a}");
    println!("{c}");
    println!("{e}");
    println!();

    let (x, y) = (&things.2, &things.4);
    println!("{x}");
    println!("{y}");
}
11
three
23

three
23

The empty tuple is called the unit

  • unit

  • ()

  • An empty pair of parentheses () creates an empty tuple, also called a unit.

  • Functions that don't return anything return the same unit value.

#![allow(clippy::let_unit_value)]
#![allow(clippy::unit_cmp)]

fn main() {
    let x = ();
    println!("{:?}", x);

    let y = ();
    println!("{:?}", y);

    let same = x == y;
    println!("{:?}", same);

    let res = empty();
    println!("{:?}", res);
    assert_eq!(res, ());

    let res = no_return();
    println!("{:?}", res);
    assert_eq!(res, ());
}

fn empty() {}

fn no_return() {
    println!("Hello World!");
}
()
()
true
()
Hello World!
()

One element tuple

  • We can create a one-element tuple by putting a comma after the element, but probably there is not much value in it.
  • It is better to just create a variable holding that single value.
fn main() {
    let thing = ("text",);
    println!("{:?}", thing);
    println!("{}", thing.0);

    let thing = "text";
    println!("{}", thing);
}
("text",)
text
text

Enumerate over vector uses tuples

fn main() {
    #[allow(clippy::useless_vec)]
    let fruits = vec!["apple", "banana", "peach"];
    for (index, name) in fruits.iter().enumerate() {
        println!("{index}) {name}");
    }
}
0) apple
1) banana
2) peach

Return multiple values from a function

fn main() {
    let (sum, diff) = calc(10, 5);
    println!("sum: {sum}  diff: {diff}");
}

fn calc(a: i32, b: i32) -> (i32, i32) {
    (a + b, a - b)
}
sum: 15  diff: 5

struct

Create simple struct

  • struct

  • A simple struct will have one or more fields. Each field has a name and a type.

  • We can then create an instance of the struct with actual values.

  • We can use the dot-notation to access the values of the fields.

  • We cannot change the values unless we declare the struct as mutable using the mut keyword.

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let a = Point { x: 2, y: 3 };
    println!("{}", a.x);
    println!("{}", a.y);

    // a.y = 7;
    // cannot assign to `a.y`, as `a` is not declared as mutable
    // println!("{}", a.y);
}
2
3

Change attributes of a mutable struct

  • mut

  • Using the mut keyword we can defined a struct to be mutable.

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let mut a = Point { x: 2, y: 3 };
    println!("{}", a.x);
    println!("{}", a.y);

    a.y = 7;
    println!("{}", a.y);
}
2
3
7

Implement a method for a struct

  • impl

  • self

  • &self

  • We can add methods to a struct using the impl keyword (we implement the method).

  • The first parameter of every method must be the variable name self that will represent the current instance.

  • We can have either self or &self. The latter will borrow the instance and it will allow us to call two methods (or the same method twice as in our example).

  • If we used only self the instance would be moved on the first method call. Rarely used.

struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn distance(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

fn main() {
    let pnt = Point { x: 2.0, y: 3.0 };
    println!("x: {}", pnt.x);
    println!("y: {}", pnt.y);

    println!("Distance from origo: {}", pnt.distance());
    println!("Distance from origo: {}", pnt.distance());
}
x: 2
y: 3
Distance from origo: 3.605551275463989
Distance from origo: 3.605551275463989

Struct method to modify fields

  • impl

  • mut

  • &mut

  • We can add methods to a struct using the impl keyword (we implement the method) that will modify the struct.

  • For this we need to write &mut self in the method and the struct instance must be also mutable.

struct Point {
    x: i32,
    y: i32,
}

impl Point {
    fn mv(&mut self, dx: i32, dy: i32) {
        self.x += dx;
        self.y += dy;
    }
}

fn main() {
    let mut pnt = Point { x: 2, y: 3 };
    println!("x: {}", pnt.x);
    println!("y: {}", pnt.y);

    pnt.mv(4, 5);
    println!("x: {}", pnt.x);
    println!("y: {}", pnt.y);
}
x: 2
y: 3
x: 6
y: 8

Struct inheritance

  • There is no inheritance among structs similar to classes in other languages. However, there is composition.
  • In other words, attributes of a struct can be both primitive types and other structs.

Struct composition: Circle

  • struct

  • impl

  • A Cricle is built from a Point and a radius.

  • We can implement (using impl) methods on both the Point and the Circle struct.

  • The Circle struct does not have a mv method.

struct Point {
    x: i32,
    y: i32,
}

struct Circle {
    point: Point,
    radius: i32,
}

impl Point {
    fn mv(&mut self, dx: i32, dy: i32) {
        self.x += dx;
        self.y += dy;
    }
}

impl Circle {
    fn area(&self) -> f32 {
        (self.radius as f32) * (self.radius as f32) * (std::f64::consts::PI as f32)
    }
}

fn main() {
    let mut a = Circle {
        point: Point { x: 2, y: 3 },
        radius: 7,
    };
    println!("{}", a.point.x);
    println!("{}", a.point.y);
    println!("{}", a.radius);
    println!("{}", a.area());
    println!();

    a.point.mv(4, 5);
    println!("{}", a.point.x);
    println!("{}", a.point.y);
    println!("{}", a.radius);
}
2
3
7
153.93805

6
8
7

Struct composition: Line

  • Not much difference from the Circle. A Line is composed of two Point structs.
struct Point {
    x: f64,
    y: f64,
}

struct Line {
    a: Point,
    b: Point,
}

impl Line {
    fn length(&self) -> f64 {
        let x_size = self.a.x - self.b.x;
        let y_size = self.a.y - self.b.y;
        let res = x_size * x_size + y_size * y_size;
        res.sqrt()
    }
}
fn main() {
    let line = Line {
        a: Point { x: 2.0, y: 3.0 },
        b: Point { x: 7.0, y: -7.0 },
    };

    println!("a.x: {}", line.a.x);
    println!("a.y: {}", line.a.y);
    println!("b.x: {}", line.b.x);
    println!("b.y: {}", line.b.y);

    println!("length: {}", line.length());
}
a.x: 2
a.y: 3
b.x: 7
b.y: -7
length: 11.180339887498949

Struct with vector of structs - Polygon

  • We can create a struct that has a single attribute which is a vector of Point instances.
  • We can then use impl to implement a function.
struct Point {
    x: i32,
    y: i32,
}

struct Polygon {
    points: Vec<Point>,
}

impl Polygon {
    fn length(&self) -> i32 {
        42
    }
}

fn main() {
    let poly = Polygon {
        points: vec![
            Point { x: 3, y: 7 },
            Point { x: 10, y: 6 },
            Point { x: 23, y: 12 },
            Point { x: -2, y: -2 },
        ],
    };

    for point in &poly.points {
        println!("x: {} y: {}", point.x, point.y)
    }

    println!("Length: {}", poly.length());
}
x: 3 y: 7
x: 10 y: 6
x: 23 y: 12
x: -2 y: -2
Length: 42

Printing struct fails

  • We can print the values of the individual attributes of a struct, but we cannot print the whole struct.
struct Animal {
    name: String,
    size: String,
    weight: i32,
}

fn main() {
    let eli = Animal {
        name: String::from("elephant"),
        size: String::from("huge"),
        weight: 100,
    };
    println!("{}", eli.name);
    println!("{}", eli.size);
    println!("{}", eli.weight);

    // println!("{}", eli);
    // `Animal<'_>` doesn't implement `std::fmt::Display`

    // println!("{:?}", eli);
    // `Animal<'_>` doesn't implement `Debug`

    // dbg!(eli);
    // `Animal<'_>` doesn't implement `Debug`
}
elephant
huge
100

Print struct - implement Display

  • impl
  • Display
  • Formatter
use std::fmt;

struct Animal {
    name: String,
    size: String,
    weight: i32,
}

impl std::fmt::Display for Animal {
    fn fmt(&self, format: &mut fmt::Formatter) -> fmt::Result {
        write!(
            format,
            "name: {}, size: {}, weight: {}",
            self.name, self.size, self.weight
        )
    }
}

fn main() {
    let eli = Animal {
        name: String::from("elephant"),
        size: String::from("huge"),
        weight: 100,
    };
    println!("{}", eli.name);
    println!("{}", eli.size);
    println!("{}", eli.weight);

    println!("{}", eli);
}
elephant
huge
100
name: elephant, size: huge, weight: 100

Debug struct - implement Debug

  • impl
  • Debug
use std::fmt;

struct Animal {
    name: String,
    size: String,
    weight: i32,
}

impl std::fmt::Debug for Animal {
    fn fmt(&self, format: &mut fmt::Formatter) -> fmt::Result {
        write!(
            format,
            "Animal {{ name: {}, size: {}, weight: {} }}",
            self.name, self.size, self.weight
        )
    }
}

fn main() {
    let eli = Animal {
        name: String::from("elephant"),
        size: String::from("huge"),
        weight: 100,
    };
    println!("{}", eli.name);
    println!("{}", eli.size);
    println!("{}", eli.weight);

    println!("{:?}", eli);
    dbg!(eli);
}
elephant
huge
100
Animal { name: elephant, size: huge, weight: 100 }
[src/main.rs:23] eli = Animal { name: elephant, size: huge, weight: 100 }

Derive Debug for struct

  • derive

  • Debug

  • We don't need to implement the fmt method of the Debug trait ourselves. We can derive it:

#[derive(Debug)]
struct Animal {
    name: String,
    size: String,
    weight: i32,
}

fn main() {
    let eli = Animal {
        name: String::from("elephant"),
        size: String::from("huge"),
        weight: 100,
    };
    println!("{}", eli.name);
    println!("{}", eli.size);
    println!("{}", eli.weight);

    println!("{:?}", eli);
    dbg!(eli);
}
elephant
huge
100
Animal { name: "elephant", size: "huge", weight: 100 }
[src/main.rs:16] eli = Animal {
    name: "elephant",
    size: "huge",
    weight: 100,
}

Struct with vector and optional value

struct Course {
    name: String,
    grades: Vec<i32>,
    final_grade: Option<i32>,
}

fn main() {
    let mut c = Course {
        name: String::from("Programming Rust"),
        grades: vec![78, 80],
        final_grade: None,
    };
    println!("{}", c.name);
    println!("{:?}", c.grades);
    println!("{:?}", c.final_grade);
    // println!("{:?}", c.final_grade.unwrap());
    // thread 'main' panicked at 'called `Option::unwrap()` on a `None` value'

    match c.final_grade {
        Some(value) => println!("Final grade is {value}"),
        None => println!("There is no final grade yet"),
    };

    c.grades.push(100);
    c.final_grade = Some(82);
    println!("{:?}", c.grades);
    println!("{:?}", c.final_grade);
    println!("{:?}", c.final_grade.unwrap());

    match c.final_grade {
        Some(value) => println!("Final grade is {value}"),
        None => println!("There is no final grade yet"),
    };
}
Programming Rust
[78, 80]
None
There is no final grade yet
[78, 80, 100]
Some(82)
82
Final grade is 82

Printing and debug-printing simple struct

#[allow(dead_code)]

struct Red(i32);

fn main() {
    #[allow(unused_variables)]
    let red = Red(10);
    println!("{}", red.0);

    // println!("{}", red);
    // `Red` doesn't implement `std::fmt::Display`

    // println!("{:?}", red);
    //  `Red` doesn't implement `Debug`
}
#![allow(dead_code)]

#[derive(Debug)]
struct Red(i32);

fn main() {
    let red = Red(10);
    //println!("{}", red);  // `Red` doesn't implement `std::fmt::Display`
    println!("{:?}", red); //  Red(10)
}
Red(10)
#[derive(Debug)]
struct Red(i32);

impl std::fmt::Display for Red {
    fn fmt(&self, format: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(format, "{}", self.0)
    }
}

fn main() {
    let red = Red(10);
    println!("{}", red); // 10
    println!("{:?}", red); //  Red(10)
}
10
Red(10)

Use a tuple as a struct to represent color

#[derive(Debug)]
struct Color(u8, u8, u8);

impl std::fmt::Display for Color {
    fn fmt(&self, format: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(format, "RGB({}, {}, {})", self.0, self.1, self.2)
    }
}

fn main() {
    let black = Color(0, 0, 0);
    println!("{}", black); // RGB(0, 0, 0)
    println!("{:?}", black); // Color(0, 0, 0)

    let white = Color(255, 255, 255);
    println!("{}", white); // RGB(255, 255, 255)
    println!("{:?}", white); // Color(255, 255, 255)

    let red = Color(255, 0, 0);
    println!("{}", red); // RGB(255, 0, 0)
    println!("{:?}", red); // Color(255, 0, 0)
}
RGB(0, 0, 0)
Color(0, 0, 0)
RGB(255, 255, 255)
Color(255, 255, 255)
RGB(255, 0, 0)
Color(255, 0, 0)

Add method to tuple-based struct

#[derive(Debug)]
struct Color(u8, u8, u8);

impl std::fmt::Display for Color {
    fn fmt(&self, format: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(format, "RGB({}, {}, {})", self.0, self.1, self.2)
    }
}

impl Color {
    fn hex(&self) -> String {
        format!("#{:X}{:X}{:X}", self.0, self.1, self.2)
    }
}

fn main() {
    let color = Color(80, 123, 241);
    println!("{}", color);
    println!("{:?}", color);

    println!("{}", color.hex());
}
RGB(80, 123, 241)
Color(80, 123, 241)
#507BF1

Struct with method

#[derive(Debug)]
struct Color {
    red: u8,
    green: u8,
    blue: u8,
}

impl std::fmt::Display for Color {
    fn fmt(&self, format: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(format, "RGB({}, {}, {})", self.red, self.green, self.blue)
    }
}

impl Color {
    fn hex(&self) -> String {
        format!("#{:X}{:X}{:X}", self.red, self.green, self.blue)
    }
}

fn main() {
    let color = Color {
        red: 80,
        green: 123,
        blue: 241,
    };
    println!("{}", color);
    println!("{:?}", color);
    println!("{}", color.hex());
}
RGB(80, 123, 241)
Color { red: 80, green: 123, blue: 241 }
#507BF1

Structs and circural references

  • Rust will make sure we cannot create circular reference in this way.
  • #[allow(unused_mut)] is needed to silence clippy, the linter
fn main() {
    let mut joe = Person {
        name: String::from("Joe"),
        partner: None,
    };
    #[allow(unused_mut)]
    let mut jane = Person {
        name: String::from("Jane"),
        partner: None,
    };
    println!("{:?}", &joe);
    println!("{:?}", &jane);
    joe.partner = Some(&jane);
    //jane.partner = Some(&joe);
    println!("{:?}", &joe);
    println!("{:?}", &jane);
}

#[derive(Debug)]
#[allow(dead_code)]
struct Person<'a> {
    name: String,
    partner: Option<&'a Person<'a>>,
}

  • Try to enable the commented out code and see the error message.
error[E0506]: cannot assign to `jane.partner` because it is borrowed
 --> src/main.rs:8:5
  |
7 |     joe.partner = Some(&jane);
  |                        ----- `jane.partner` is borrowed here
8 |     jane.partner = Some(&joe);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^ `jane.partner` is assigned to here but it was already borrowed
9 |     dbg!(&joe);
  |          ---- borrow later used here

For more information about this error, try `rustc --explain E0506`.
error: could not compile `circural-references` (bin "circural-references") due to previous error

new method with default values for struct

  • new

  • We use the new name by convention. It has no special meaning in Rust.

#[derive(Debug)]
#[allow(dead_code)]
struct Something {
    name: String,
    number: i32,
}

impl Something {
    pub fn new() -> Something {
        Something {
            name: String::new(),
            number: 0,
        }
    }
}

fn main() {
    let sg = Something {
        name: String::from("Foo Bar"),
        number: 42,
    };
    println!("{:?}", sg);

    let new = Something::new();
    println!("{:?}", new);
}

The new method has no special feature

  • new

  • We could use any other name instead of new. For example we could use qqrq as well.

  • The name new is only to give the power of familiarity.

#[derive(Debug)]
#[allow(dead_code)]
struct Something {
    name: String,
    number: i32,
}

impl Something {
    pub fn qqrq() -> Something {
        Something {
            name: String::new(),
            number: 0,
        }
    }
}

fn main() {
    let sg = Something {
        name: String::from("Foo Bar"),
        number: 42,
    };
    println!("{:?}", sg);

    let new = Something::qqrq();
    println!("{:?}", new);
}

Default values

  • impl

  • Default

  • We can implement the Default trait and then we can use

..Something::default()

to fill in the blank fields with the default values.

#[derive(Debug)]
#[allow(dead_code)]
struct Something {
    name: String,
    number: i32,
}

impl Default for Something {
    fn default() -> Something {
        Something {
            name: String::from("Rust"),
            number: 19,
        }
    }
}

fn main() {
    let sg = Something {
        name: String::from("Foo Bar"),
        number: 42,
        //..Default::default()   // We can enable this or leave it out, does not matter.
    };
    println!("{:?}", sg);
    assert_eq!(sg.name, "Foo Bar");
    assert_eq!(sg.number, 42);

    let empty = Something {
        ..Default::default()
    };
    println!("{:?}", empty);
    assert_eq!(empty.name, "Rust");
    assert_eq!(empty.number, 19);

    let with_name = Something {
        name: String::from("Hello"),
        ..Default::default()
    };
    println!("{:?}", with_name);
    assert_eq!(with_name.name, "Hello");
    assert_eq!(with_name.number, 19);

    let with_number = Something {
        number: 42,
        ..Default::default()
    };
    println!("{:?}", with_number);
    assert_eq!(with_number.name, "Rust");
    assert_eq!(with_number.number, 42);
}

Empty string and zero as default values

  • derivable_impls

  • In this special case the default values we set in the implementation of the Default trait happen to be the default values of each primitive type. (empty string for strings and 0 for numbers).

  • In this case clippy will indicate that we don't need to implement Default by ourselves.

  • In this example we silences clippy, in the next example we derive from Default.

#![allow(clippy::derivable_impls)]

#[derive(Debug)]
#[allow(dead_code)]
struct Something {
    name: String,
    number: i32,
}

impl Default for Something {
    fn default() -> Something {
        Something {
            name: String::new(),
            number: 0,
        }
    }
}

fn main() {
    let sg = Something {
        name: String::from("Foo Bar"),
        number: 42,
        //..Default::default()
    };
    println!("{:?}", sg);
    assert_eq!(sg.name, "Foo Bar");
    assert_eq!(sg.number, 42);

    let empty = Something {
        ..Default::default()
    };
    println!("{:?}", empty);
    assert_eq!(empty.name, "");
    assert_eq!(empty.number, 0);

    let with_name = Something {
        name: String::from("Hello"),
        ..Default::default()
    };
    println!("{:?}", with_name);
    assert_eq!(with_name.name, "Hello");
    assert_eq!(with_name.number, 0);

    let with_number = Something {
        number: 42,
        ..Default::default()
    };
    println!("{:?}", with_number);
    assert_eq!(with_number.name, "");
    assert_eq!(with_number.number, 42);
}

Derived Default values

#[derive(Debug, Default)]
#[allow(dead_code)]
struct Something {
    name: String,
    number: i32,
}

fn main() {
    let sg = Something {
        name: String::from("Foo Bar"),
        number: 42,
        // ..Default::default()
    };
    println!("{:?}", sg);
    assert_eq!(sg.name, "Foo Bar");
    assert_eq!(sg.number, 42);

    let empty = Something {
        ..Default::default()
    };
    println!("{:?}", empty);
    assert_eq!(empty.name, "");
    assert_eq!(empty.number, 0);

    let with_name = Something {
        name: String::from("Hello"),
        ..Default::default()
    };
    println!("{:?}", with_name);
    assert_eq!(with_name.name, "Hello");
    assert_eq!(with_name.number, 0);

    let with_number = Something {
        number: 42,
        ..Default::default()
    };
    println!("{:?}", with_number);
    assert_eq!(with_number.name, "");
    assert_eq!(with_number.number, 42);
}

Default for composite struct

#![allow(clippy::derivable_impls)]

#[derive(Debug)]
#[allow(dead_code)]
struct Something {
    name: String,
    input: Input,
}

#[derive(Debug)]
#[allow(dead_code)]
struct Input {
    name: String,
}

impl Something {
    pub fn new() -> Something {
        Something {
            name: String::new(),
            input: Input::new(),
        }
    }
}

impl Input {
    pub fn new() -> Input {
        Input {
            name: String::new(),
        }
    }
}

impl Default for Something {
    fn default() -> Something {
        Something {
            name: String::new(),
            input: Input { ..Input::default() },
        }
    }
}

impl Default for Input {
    fn default() -> Input {
        Input {
            name: String::new(),
        }
    }
}

fn main() {
    let sg = Something {
        name: String::from("Foo Bar"),
        input: Input {
            name: String::from("input text"),
        },
    };
    println!("{:?}", sg);

    let new = Something::new();
    println!("{:?}", new);

    let empty = Something {
        ..Something::default()
    };
    println!("{:?}", empty);

    let with_name = Something {
        name: String::from("Hello"),
        ..Something::default()
    };
    println!("{:?}", with_name);
}
[src/main.rs:58:5] sg = Something {
    name: "Foo Bar",
    input: Input {
        name: "input text",
    },
}
[src/main.rs:61:5] new = Something {
    name: "",
    input: Input {
        name: "",
    },
}
[src/main.rs:66:5] empty = Something {
    name: "",
    input: Input {
        name: "",
    },
}
[src/main.rs:73:5] with_name = Something {
    name: "Hello",
    input: Input {
        name: "",
    },
}

Compare structs for Equality

  • Eq

  • PartialEq

  • Each data type in Rust either implements Eq or PartialEq to allow users to check if two objects of the same type are equal using either the == operator or the eq method.

  • When creating a struct it does not automatically implement these traits, but we can add them.

  • Primitive data types such as integers and strings implement both Eq and PartialEq.

  • float on the other hand only implements PartialEq as a float can also be NaN that would break Eq.

  • We can add the Eq trait to any struct and if all the elements of the struct implement Eq then we can add that too:

  • It will automatically provide us with the possibility to use == or eq (or != or ne for that matter) on the values of that type.

  • However Eq is mostly just an indication to the compiler, the actual implementation is in PartialEq so we need to add that too.

  • In order for two objects of this type to be equal, all the fields have to be equal.

#[derive(PartialEq, Eq)]
struct Thing {
    name: String,
    number: i32,
}

fn main() {
    let a = Thing {
        name: String::from("Foo"),
        number: 42,
    };

    let b = Thing {
        name: String::from("Foo"),
        number: 42,
    };

    let c = Thing {
        name: String::from("Foo1"),
        number: 42,
    };

    println!("{}", a == b);
    println!("{}", a == c);

    // We cannot compare which is bigger as we have not implemented (or derived from) Ord or PartialOrd.
    // println!("{}", a < c);
}
true
false

Compare structs for Equality - manual implementation

  • PartialEq

  • eq

  • The #[allow(dead_code)] part is only needed as in this example we never use the name field. In real code you will probably not need it.

#[allow(dead_code)]
struct Thing {
    name: String,
    number: i32,
}

impl PartialEq for Thing {
    fn eq(&self, other: &Self) -> bool {
        self.number == other.number
    }
}

fn main() {
    let a = Thing {
        name: String::from("Foo"),
        number: 42,
    };

    let b = Thing {
        name: String::from("Foo"),
        number: 42,
    };

    let c = Thing {
        name: String::from("Foo1"),
        number: 42,
    };

    println!("{}", a == b);
    println!("{}", a == c);

    // We cannot compare which is bigger as we have not implemented
    // (or derived from) Ord or PartialOrd.
    // println!("{}", a < c);
}
true
true

Compare structs for partial equality - PartialEq

  • struct

  • PartialEq

  • PartialEq

  • the trait Eq is not implemented for f32

#[derive(PartialEq)]
struct Thing {
    name: String,
    number: i32,
    float: f32,
}

fn main() {
    let a = Thing {
        name: String::from("Foo"),
        number: 42,
        float: 1.0,
    };

    let b = Thing {
        name: String::from("Foo"),
        number: 42,
        float: 1.0,
    };

    let c = Thing {
        name: String::from("Foo1"),
        number: 42,
        float: 1.0,
    };

    println!("{}", a == b);
    println!("{}", a == c);

    // must implement `PartialOrd`
    // println!("{}", a < c);
}
true
false

Compare structs for ordering (sorting) - Ord

  • Ord

  • PartialOrd

  • In order to be able to decide which object is "bigger" or "smaller" than the other one we need the Ord trait that requires PartialOrd trait and the Eq and PartialEq traits.

  • This will allow use to sort the values.

  • Comaring the fields happen in the order the appear in the definition of ths struct. In our case Rust forst compares the 'number' fields. The 'name' fields are only compared if the 'number' fields are equal.

#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Thing {
    number: i32,
    name: String,
}

fn main() {
    let a = Thing {
        number: 42,
        name: String::from("Foo"),
    };

    let b = Thing {
        number: 43,
        name: String::from("Foo"),
    };

    let c = Thing {
        number: 42,
        name: String::from("Bar"),
    };

    let d = Thing {
        number: 43,
        name: String::from("Bar"),
    };

    println!("{}", a < b); // becasue 42 < 43
    println!("{}", c < b); // becasue 42 < 43 ( Bar < Foo is not even checked here)
    println!("{}", c < a); // because Bar < Foo
    println!("{}", c < d); // because 42 < 43
    println!("{}", a < d); // because 42 < 43
                           // the comparisions is in the order of declaraion of the fields
                           // So the `name` field is not checked here
}
true
true
true
true
true

Compare structs for partial ordering (sorting) - PartialOrd

  • PartialOrd

  • PartialOrd

  • Neither the Eq nor the Ord traits are implemented for floats.

#[derive(PartialEq, PartialOrd)]
struct Thing {
    number: i32,
    float: f32,
}

fn main() {
    let a = Thing {
        number: 42,
        float: 1.0,
    };

    let b = Thing {
        number: 43,
        float: 1.0,
    };

    let c = Thing {
        number: 42,
        float: 0.9,
    };

    let d = Thing {
        number: 43,
        float: 0.9,
    };

    println!("{}", a < b); // becasue 42 < 43
    println!("{}", c < b); // becasue 42 < 43 and also 0.9 < 1.0
    println!("{}", c < a); // because 0.0 < 1.0
    println!("{}", c < d); // because 42 < 43
    println!("{}", a < d); // because 42 < 43
                           // the comparisions is in the order of declaraion of the fields
}
true
true
true
true
true

Manually implement ordering

  • PartialOrd

  • PartialEq

  • partial_cmp

  • eq

  • In rare case we might want to have an ordering that is not the same as the default implementation of PartialOrd. In these cases we can implement it ourselves.

  • For this we need to implement both PartialEq and PartialOrd.

  • In our case the functions only take into account the height field.

  • the #[allow(dead_code)] is only needed in this example because we never access the id and name fields.

use std::cmp::Ordering;

#[allow(dead_code)]
struct Person {
    id: u32,
    name: String,
    height: u32,
}

impl PartialOrd for Person {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.height.cmp(&other.height))
    }
}

impl PartialEq for Person {
    fn eq(&self, other: &Self) -> bool {
        self.height == other.height
    }
}

fn main() {
    let a = Person {
        id: 1,
        name: String::from("Foo"),
        height: 160,
    };

    let b = Person {
        id: 1,
        name: String::from("Foo"),
        height: 180,
    };

    let c = Person {
        id: 1,
        name: String::from("Foo"),
        height: 160,
    };

    let x = Person {
        id: 2,
        name: String::from("Bar"),
        height: 180,
    };

    println!("{}", a < b);
    println!("{}", a == c);
    println!("{}", b == x);
}
true
true
true

Copy attributes from struct instance

macro_rules! prt {
    ($name: expr) => {
        println!("{:?} {:p}", $name, &$name);
    };
}

#[allow(dead_code)]
#[derive(Debug)]
struct Point {
    x: u32,
    y: u32,
    z: u32,
}

fn main() {
    let point1 = Point { x: 1, y: 2, z: 3 };
    prt!(point1);

    let point2 = Point { ..point1 };
    prt!(point2);

    let point3 = Point { x: 4, ..point1 };
    prt!(point3);
}
Point { x: 1, y: 2, z: 3 } 0x7fff5be3ba14
Point { x: 1, y: 2, z: 3 } 0x7fff5be3ba9c
Point { x: 4, y: 2, z: 3 } 0x7fff5be3bb24

Drop - destructor

  • Drop

  • drop

  • Implement the Drop trait

  • By default when there is a panic! Rust will unwind the allocated memory and it will call the drop method on each object it encounters. We can set the panic compiler option to 'abort' in the Cargo.toml file to make Rust exit without unwinding. This will make shutting down the program faster, but in this case the drop methods will not be called.

#![allow(dead_code, unused_variables)]

struct HasDrop {
    name: String,
}

impl Drop for HasDrop {
    fn drop(&mut self) {
        println!("Dropping HasDrop! {}", self.name);
    }
}

struct HasTwoDrops {
    one: HasDrop,
    two: HasDrop,
}

impl Drop for HasTwoDrops {
    fn drop(&mut self) {
        println!("Dropping HasTwoDrops!");
    }
}

fn main() {
    let hd = HasDrop {
        name: String::from("Foo"),
    };
    let _hd = HasTwoDrops {
        one: HasDrop {
            name: String::from("first"),
        },
        two: HasDrop {
            name: String::from("second"),
        },
    };

    calc(3, 0);
}

fn calc(x: i32, y: i32) -> i32 {
    if y == 0 {
        panic!("oups");
    }
    x / y
}
[package]
name = "drop-demo"
version = "0.1.0"
edition = "2021"

[dependencies]

#[profile.dev]
#panic = 'abort'

#[profile.release]
#panic = 'abort'

Exercise - struct for contact info

Create the code in steps. In each step create the struct add some data, and print it.

    1. At first define a person that has a name, an id.
    1. Each person can have several phone numbers. Each phone number has a type and a number. The type might be "Home" or "Work" or "Mobile".
    1. We might want to add some notes to some of the people.
    1. Some people might have a title (e.g. "Doctor", "Prof")

Solution - struct for contact info

#[allow(dead_code)]
#[derive(Debug)]
enum Title {
    Doctor,
    Prof,
}

#[allow(dead_code)]
#[derive(Debug)]
enum PhoneType {
    Home,
    Work,
    Mobile,
}

#[allow(dead_code)]
#[derive(Debug)]
struct Phone {
    number: String,
    ptype: PhoneType,
}

#[allow(dead_code)]
#[derive(Debug)]
struct Person {
    name: String,
    id: String,
    phones: Vec<Phone>,
    notes: Option<String>,
    title: Option<Title>,
}

fn main() {
    let joe = Person {
        name: String::from("Joe"),
        id: String::from("123456"),
        phones: vec![Phone {
            number: String::from("055-1234567"),
            ptype: PhoneType::Home,
        }],
        notes: None,
        title: None,
    };

    let jane = Person {
        name: String::from("Jane"),
        id: String::from("678"),
        phones: vec![Phone {
            number: String::from("123"),
            ptype: PhoneType::Work,
        }],
        notes: Some(String::from("was a student")),
        title: Some(Title::Doctor),
    };

    println!("{:#?}", joe);
    println!("{:#?}", jane);
}
Person {
    name: "Joe",
    id: "123456",
    phones: [
        Phone {
            number: "055-1234567",
            ptype: Home,
        },
    ],
    notes: None,
    title: None,
}
Person {
    name: "Jane",
    id: "678",
    phones: [
        Phone {
            number: "123",
            ptype: Work,
        },
    ],
    notes: Some(
        "was a student",
    ),
    title: Some(
        Doctor,
    ),
}

Read from a file and return a struct

We have some data file that looks like this:

{% embed include file="src/examples/struct/read-from-file/animals.txt)

We would like to create a function that will read the file and return a vector of structs representing the data.

use std::error::Error;
use std::fs::File;
use std::io::{BufRead, BufReader};

#[derive(Debug)]
#[allow(dead_code)]
struct Animal {
    name: String,
    legs: u8,
}

fn main() {
    let animals = read_file();
    println!("{:?}", animals);
}

fn read_file() -> Result<Vec<Animal>, Box<dyn Error>> {
    let filename = "animals.txt";
    let fh = File::open(filename)?;
    let reader = BufReader::new(fh);
    let mut animals = vec![];
    for line in reader.lines() {
        let line = line?;
        let pieces = line.split(',').collect::<Vec<_>>();
        animals.push(Animal {
            name: pieces[0].to_string(),
            legs: pieces[1].parse()?,
        })
    }

    Ok(animals)
}

In this case we cannot defined the name field to be &str as that will not work well with the function reading the data from a file. The read_file will read in the content of the file into owned string. From that we could create structs that have references in them, but when we return the structs from the functions the owner of that data will go out of scope and we'll get a compilation error: borrowed value does not live long enough

Converting between types: The From and Into traits

  • from
  • into

You have probably seen code using ::from() or .into() in some code. These are methods of the From and Into traits. You always implement only one of them.

From and Into for String and &str

  • &str

  • String

  • from

  • into

  • We can create a String from an &str by using the String::from() method because String implements the From trait for &str.

  • We can also use the into method, but then we must tell Rust the expected type.

  • For some reason we cannot use the Turbofish syntax.

fn main() {
    let a = "Hello World!";
    println!("{a}");

    let b = String::from(a);
    println!("{b}");

    let c: String = a.into();
    println!("{c}");

    // let d = a.into::<String>();
    // method takes 0 generic arguments but 1 generic argument was supplied
    // println!("{d}");

    let e = Into::<String>::into(a);
    println!("{e}");
}

Implementing the From trait for 2D and 3D point structs

  • From
  • from
  • into
#![allow(dead_code)]

#[derive(Debug)]
struct Point2D {
    x: f64,
    y: f64,
}

#[derive(Debug)]
struct Point3D {
    x: f64,
    y: f64,
    z: f64,
}

impl From<&Point2D> for Point3D {
    fn from(item: &Point2D) -> Point3D {
        Point3D {
            x: item.x,
            y: item.y,
            z: 0.0,
        }
    }
}

fn main() {
    let p2 = Point2D { x: -1.1, y: 2.1 };
    println!("{p2:?}");

    let p3 = Point3D {
        x: 1.0,
        y: 2.0,
        z: 3.0,
    };
    println!("{p3:?}");

    // let d3 = Point3D::from(p3); // This works
    // let d3 = Point3D::from(p2); // Error: mismatched types  expected `Point3D`, found `Point2D`
    // TODO should no it also mention the possibility to create a From or Into trait?

    let d3 = Point3D::from(&p2);
    println!("{d3:?}");

    let d3: Point3D = (&p2).into();
    println!("{d3:?}");

    //let x3 = (&p2).into::<Point3D>();
    let x3 = Into::<Point3D>::into(&p2);
    println!("{x3:?}");
}

Other: Struct and type alias - Polygon

  • struct

  • type

  • The simplest way to represent a polygon (a series of points) is a vector of Point instances.

  • We can even give it a name using the type keyword.

  • Despite its name it does not create a new type, just an alias.

  • That's why we cannot use impl to add a method.

struct Point {
    x: i32,
    y: i32,
}

type Polygon = Vec<Point>;

// cannot define inherent `impl` for a type outside of the crate where the type is defined
// impl Polygon {
//     fn length(&self) {
//     }
// }

fn main() {
    let poly: Polygon = vec![
        Point { x: 3, y: 7 },
        Point { x: 10, y: 6 },
        Point { x: 23, y: 12 },
        Point { x: -2, y: -2 },
    ];

    for point in poly {
        println!("x: {} y: {}", point.x, point.y)
    }
}
x: 3 y: 7
x: 10 y: 6
x: 23 y: 12
x: -2 y: -2

Other: Struct duplicate

  • This seems to be an old example showing that if we don't compose one struct from another then we have to implement everything in both cases.
  • In this case the Circle struct has its own x and y attribute and its own mv method.
struct Point {
    x: i32,
    y: i32,
}

struct Circle {
    x: i32,
    y: i32,
    radius: i32,
}

impl Point {
    fn mv(&mut self, dx: i32, dy: i32) {
        self.x += dx;
        self.y += dy;
    }
}

impl Circle {
    fn mv(&mut self, dx: i32, dy: i32) {
        self.x += dx;
        self.y += dy;
    }
    fn area(&self) -> f32 {
        (self.radius as f32) * (self.radius as f32) * (std::f64::consts::PI as f32)
    }
}

fn main() {
    let mut p = Point { x: 2, y: 3 };
    println!("{}", p.x);
    println!("{}", p.y);
    println!();

    p.mv(4, 5);
    println!("{}", p.x);
    println!("{}", p.y);
    println!("----");

    let mut a = Circle {
        x: 2,
        y: 3,
        radius: 7,
    };
    println!("{}", a.x);
    println!("{}", a.y);
    println!("{}", a.radius);
    println!("{}", a.area());
    println!();

    a.mv(4, 5);
    println!("{}", a.x);
    println!("{}", a.y);
    println!("{}", a.radius);
}
2
3

6
8
----
2
3
7
153.93805

6
8
7

Other: Multiple referene to a struct

#[derive(Debug)]
#[allow(dead_code)]
struct Something {
    number: i32,
    text: String,
    numbers: Vec<i32>,
}

fn main() {
    let a = Something {
        number: 2,
        text: String::from("blue"),
        numbers: vec![5, 6],
    };
    println!("{:?}", &a);

    let b = &a;
    println!("{:?}", &b);
    println!("{:?}", &a);
}

Other: Print struct (Point)

  • std::fmt::Display
  • Display
use std::fmt;

struct Point {
    x: i32,
    y: i32,
}

impl std::fmt::Display for Point {
    fn fmt(&self, format: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(format, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let pnt = Point { x: 2, y: 3 };

    println!("The point is: {pnt}");
}
The point is: (2, 3)

Other: Debug struct (Point)

  • std::fmt::Debug
  • Debug
use std::fmt;

struct Point {
    x: i32,
    y: i32,
}

impl std::fmt::Debug for Point {
    fn fmt(&self, format: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(format, "Point(x: {}, y: {})", self.x, self.y)
    }
}

fn main() {
    let pnt = Point { x: 2, y: 3 };

    dbg!(pnt);
}
[examples/struct/debug_point.rs:17] pnt = Point(x: 2, y: 3)

Vectors in Rust

Fixed vector of numbers

  • vec!

  • len

  • A Vec vector is a series of values of the same type.

  • We can initialize a vector using the vec! macro.

  • We can get the length of the vector using the len method.

  • We cannot print a vector with the simle {} placeholder because Display is not implemented for it.

  • However we can use the {:?} or the {:#?} placeholders.

  • By default vectors are immutable.

fn main() {
    let numbers = vec![10, 11, 12];
    println!("{}", numbers.len());

    // println!("{}", numbers);
    // `Vec<{integer}>` doesn't implement `std::fmt::Display`
    // `Vec<{integer}>` cannot be formatted with the default formatter

    println!("{:?}", numbers);
    println!("{:#?}", numbers);
}
3
[10, 11, 12]
[
    10,
    11,
    12,
]

Iterate over elements of vector using for-loop

  • for

  • in

  • We can iterate over the elements of a vector using the for .. in .. loop construct.

fn main() {
    let numbers = vec![10, 11, 12];
    for num in numbers {
        println!("{}", num);
    }
}
10
11
12

Mutable vector of numbers, append (push) values

  • mut

  • push

  • append

  • We can make a vector mutable by using the mut keyword.

  • We can append an element to the end of the vector using the push method.

fn main() {
    let mut numbers = vec![10, 11, 12];
    println!("{}", numbers.len());
    println!("{:?}", numbers);

    numbers.push(23);

    println!("{}", numbers.len());
    println!("{:?}", numbers);
}
3
[10, 11, 12]
4
[10, 11, 12, 23]

Mutable empty vector for numbers (push)

  • push

  • mut

  • vec!

  • We can also create a mutable empty vector without even declaring the types of the values.

  • When we push the first value that's how Rust will know what is the type of the elements of the vector.

  • Trying to push values of other types would then generate a compilation error.

fn main() {
    let mut names = vec![];
    println!("{:?}", names);

    names.push(23);

    // names.push("hello"); // error[E0308]: mismatched types - expected integer, found `&str`
    // names.push(3.14);    // error[E0308]: mismatched types - expected integer, found floating-point number
    names.push(19);

    println!("{:?}", names);
    for name in names {
        println!("{}", name);
    }
}
[]
[23, 19]
23
19

Mutable empty vector for strings

  • This is the same example as before, but this time we push a string first.
fn main() {
    let mut names = vec![];
    println!("{:?}", names);

    names.push(String::from("apple"));
    // names.push(23);   // error[E0308]: mismatched types - expected `&str`, found integer
    // names.push(3.14); // error[E0308]: mismatched types - expected `&str`, found floating-point number

    names.push(String::from("banana"));

    println!("{:?}", names);
    for name in names {
        println!("{}", name);
    }
}
[]
["apple", "banana"]
apple
banana

Mutable empty vector with type definition

  • vec!

  • push

  • We can also declare the type of the values in the vector. It might make the code more readable.

  • And it might eliminate the need to explicitely tell a parse method the target value type.

  • In this case Rust will see the parse method and because the result is pushed onto a vector of i32 numbers it will know that i32 is the type of the number variable.

fn main() {
    let mut numbers: Vec<i8> = vec![];
    println!("{:?}", numbers);

    let input = "42";

    // names.push(input);
    // mismatched types

    let number = input.parse().unwrap();
    numbers.push(number);

    println!("{:?}", numbers);

    for num in numbers {
        println!("{}", num);
    }
}
[]
[42]
42

Mutable vector of strings

fn main() {
    let mut names = vec![String::from("snake")];
    names.push(String::from("crab"));
    println!("{:?}", names);
    for name in names {
        println!("{}", name);
    }
}
["snake", "crab"]
snake
crab

Remove the last element using pop, reduce capacity

  • pop
  • len
  • capacity
  • shrink_to_fit
fn main() {
    let mut animals = vec![
        String::from("dog"),
        String::from("cat"),
        String::from("camel"),
        String::from("crab"),
        String::from("snake"),
    ];
    println!("{} {}", animals.len(), animals.capacity());
    match animals.pop() {
        None => println!("No animals"),
        Some(last) => println!("last: {last}"),
    }
    println!("{} {}", animals.len(), animals.capacity());

    animals.shrink_to_fit();
    println!("{} {}", animals.len(), animals.capacity());
}
5 5
last: snake
4 5
4 4

Stack and the capacity of a vector

  • push
  • pop
  • len
  • capacity

A little example showing how a vector growth its length (len) and capacity as we push more and more values on it.

Then how it reduces the length but keeps the allocated capacity as we pop out elements.

Finally how it can reduce the capacity when calling shrink_to_fit.

fn main() {
    let mut stack = vec![];
    show(&stack);

    stack.push(String::from("dog"));
    show(&stack);

    stack.push(String::from("snake"));
    show(&stack);

    stack.push(String::from("cat"));
    show(&stack);

    stack.push(String::from("turle"));
    show(&stack);

    stack.push(String::from("camel"));
    show(&stack);

    stack.pop();
    show(&stack);

    stack.pop();
    show(&stack);

    stack.shrink_to_fit();
    show(&stack);
}

fn show(stack: &Vec<String>) {
    println!("{}, {}, {:?}", stack.len(), stack.capacity(), stack);
}
0, 0, []
1, 4, ["dog"]
2, 4, ["dog", "snake"]
3, 4, ["dog", "snake", "cat"]
4, 4, ["dog", "snake", "cat", "turle"]
5, 8, ["dog", "snake", "cat", "turle", "camel"]
4, 8, ["dog", "snake", "cat", "turle"]
3, 8, ["dog", "snake", "cat"]
3, 3, ["dog", "snake", "cat"]

Extend vectors of numbers (combining two vectors)

  • extend
macro_rules! prt {
    ($var:expr) => {
        println!(
            "{:2} {:2} {:p} {:15?} '{:?}'",
            $var.len(),
            $var.capacity(),
            &$var,
            $var.as_ptr(),
            $var
        );
    };
}

fn main() {
    let mut numbers1 = vec![10, 11];
    prt!(numbers1);

    let mut numbers2 = vec![20, 21, 22];
    prt!(numbers2);

    numbers1.extend(&numbers2);
    prt!(numbers1);
    prt!(numbers2);

    numbers2[1] = 33;
    prt!(numbers1);
    prt!(numbers2);
}
 2  2 0x7ffd399e1988  0x647d5d1f5b80 '[10, 11]'
 3  3 0x7ffd399e1d88  0x647d5d1f5ba0 '[20, 21, 22]'
 5  5 0x7ffd399e1988  0x647d5d1f5b80 '[10, 11, 20, 21, 22]'
 3  3 0x7ffd399e1d88  0x647d5d1f5ba0 '[20, 21, 22]'
 5  5 0x7ffd399e1988  0x647d5d1f5b80 '[10, 11, 20, 21, 22]'
 3  3 0x7ffd399e1d88  0x647d5d1f5ba0 '[20, 33, 22]'

Extend vector of Strings (combining two vectors)

  • extend
macro_rules! prt {
    ($var:expr) => {
        println!(
            "{:2} {:2} {:p} {:15?} '{:?}'",
            $var.len(),
            $var.capacity(),
            &$var,
            $var.as_ptr(),
            $var
        );
    };
}

fn main() {
    let mut fruits1 = vec![String::from("apple"), String::from("banana")];
    prt!(fruits1);

    let mut fruits2 = vec![
        String::from("peach"),
        String::from("kiwi"),
        String::from("mango"),
    ];
    prt!(fruits2);

    fruits1.extend(fruits2.clone());
    prt!(fruits1);
    prt!(fruits2);

    prt!(fruits1[3]);
    prt!(fruits2[1]);

    fruits2[1] =
        String::from("some fruit with a very long name that requires more memory than we have");
    prt!(fruits1[3]);
    prt!(fruits2[1]);

    prt!(fruits1);
    prt!(fruits2);
}
 2  2 0x7ffd6a95da18  0x58e4378e9b80 '["apple", "banana"]'
 3  3 0x7ffd6a95de50  0x58e4378e9c00 '["peach", "kiwi", "mango"]'
 5  5 0x7ffd6a95da18  0x58e4378e9d60 '["apple", "banana", "peach", "kiwi", "mango"]'
 3  3 0x7ffd6a95de50  0x58e4378e9c00 '["peach", "kiwi", "mango"]'
 4  4 0x58e4378e9da8  0x58e4378e9d20 '"kiwi"'
 4  4 0x58e4378e9c18  0x58e4378e9c70 '"kiwi"'
 4  4 0x58e4378e9da8  0x58e4378e9d20 '"kiwi"'
71 71 0x58e4378e9c18  0x58e4378e9cb0 '"some fruit with a very long name that requires more memory than we have"'
 5  5 0x7ffd6a95da18  0x58e4378e9d60 '["apple", "banana", "peach", "kiwi", "mango"]'
 3  3 0x7ffd6a95de50  0x58e4378e9c00 '["peach", "some fruit with a very long name that requires more memory than we have", "mango"]'

Append vector of Strings (moving over elements)

  • append

  • This will empty the second vector.

macro_rules! prt {
    ($var:expr) => {
        println!(
            "{:2} {:2} {:p} {:15?} '{:?}'",
            $var.len(),
            $var.capacity(),
            &$var,
            $var.as_ptr(),
            $var
        );
    };
}

fn main() {
    let mut fruits1 = vec![String::from("apple"), String::from("banana")];
    prt!(fruits1);

    let mut fruits2 = vec![
        String::from("peach"),
        String::from("kiwi"),
        String::from("mango"),
    ];
    prt!(fruits2);

    prt!(fruits2[1]);

    fruits1.append(&mut fruits2);
    prt!(fruits1[3]);

    prt!(fruits1);
    prt!(fruits2);
}
 2  2 0x7ffcda3de400  0x5aae5be7fb80 '["apple", "banana"]'
 3  3 0x7ffcda3de838  0x5aae5be7fc00 '["peach", "kiwi", "mango"]'
 4  4 0x5aae5be7fc18  0x5aae5be7fc70 '"kiwi"'
 4  4 0x5aae5be7fcf8  0x5aae5be7fc70 '"kiwi"'
 5  5 0x7ffcda3de400  0x5aae5be7fcb0 '["apple", "banana", "peach", "kiwi", "mango"]'
 0  3 0x7ffcda3de838  0x5aae5be7fc00 '[]'

Split string into iterator

fn main() {
    let text = String::from("mouse cat   oliphant");
    println!("{text}");

    let parts = text.split(' ');
    //println!("{:?}", parts);
    for part in parts {
        println!("{}", part);
    }
    println!("-------");

    let parts = text.split_whitespace();
    //println!("{:?}", parts);
    for part in parts {
        println!("{}", part);
    }

    //println!("{}", parts[0]); // cannot index into a value of type `SplitWhitespace<'_>`
}
mouse cat   oliphant
mouse
cat


oliphant
-------
mouse
cat
oliphant

Split string into vector

  • split
  • vec
fn main() {
    let text = String::from("One=Two=Three");

    let parts: Vec<&str> = text.split('=').collect();
    println!("{}", parts[0]);
    println!("{}", parts[1]);
    println!("{}", parts[2]);
    println!("-------");

    let text = String::from("mouse cat   oliphant");
    let parts = text.split_whitespace().collect::<Vec<_>>();
    println!("{:?}", parts);
    println!("{}", parts[0]);
}
One
Two
Three
-------
["mouse", "cat", "oliphant"]
mouse

Sort vector of numbers

  • sort
fn main() {
    let mut numbers = vec![10, 12, 11];
    println!("{:?}", numbers);
    numbers.sort();
    println!("{:?}", numbers);
}
[10, 12, 11]
[10, 11, 12]

Sort vector of strings using sorting condition

  • sort
  • sort_by
fn main() {
    let mut animals = get_animals();

    println!("{animals:?}");
    assert_eq!(animals, ["snake", "crab", "elephant", "lizard"]);

    animals.sort();
    println!("{animals:?}");
    assert_eq!(animals, ["crab", "elephant", "lizard", "snake"]);

    #[allow(clippy::unnecessary_sort_by)]
    animals.sort_by(|a, b| a.len().cmp(&b.len()));
    println!("{animals:?}");
    assert_eq!(animals, ["crab", "snake", "lizard", "elephant"]);

    let mut animals = get_animals();
    animals.sort_by_key(|a| a.len());
    println!("{animals:?}");
    assert_eq!(animals, ["crab", "snake", "lizard", "elephant"]);
}

fn get_animals() -> Vec<String> {
    vec![
        String::from("snake"),
        String::from("crab"),
        String::from("elephant"),
        String::from("lizard"),
    ]
}
["snake", "crab", "elephant", "lizard"]
["crab", "elephant", "lizard", "snake"]
["crab", "snake", "lizard", "elephant"]
["crab", "snake", "lizard", "elephant"]

Exercise: Median

  • Write a function that given a vector of integers it will return the median.

Exercise: ROT13

  • Implement a function that given a string will return it ROT13 encrypted version.
  • If we call the function again with the result we should get back the original string.

Solution: Median

fn main() {
    let numbers = vec![10, 12, 11];
    println!("{:?}", numbers);
    println!("{}", median(&numbers));
    println!("{:?}", numbers);

    let numbers = vec![10, 12, 13, 11];
    println!("{:?}", numbers);
    println!("{}", median(&numbers));
    println!("{:?}", numbers);
}

fn median(numbers: &[i32]) -> i32 {
    let mut numbers = numbers.to_owned();
    numbers.sort();
    let middle = numbers.len() / 2;
    numbers[middle]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn check_median() {
        let numbers = vec![10, 12, 11];
        let med = median(&numbers);
        assert_eq!(med, 11);
        assert_eq!(numbers, vec![10, 12, 11]);

        let numbers = vec![10, 12, 13, 11];
        let med = median(&numbers);
        assert_eq!(med, 12);
        assert_eq!(numbers, vec![10, 12, 13, 11]);
    }
}
[10, 12, 11]
11
[10, 12, 11]
[10, 12, 13, 11]
12
[10, 12, 13, 11]

Solution: ROT13

fn main() {
    let text = String::from("Hello World!");
    println!("{text}");
    let encrypted = rot13(text);
    println!("{encrypted}");
    let decrypted = rot13(encrypted);
    println!("{decrypted}");

    //let equal = text == decrypted;
    //println!("{equal}");
}

fn rot13(text: String) -> String {
    let mut chars: Vec<char> = vec![];
    let a = 'a' as u32;
    let z = 'z' as u32;
    let aa = 'A' as u32;
    let zz = 'Z' as u32;
    for ch in text.chars() {
        let mut code = ch as u32;
        // dbg!(code);
        if a <= code && code <= z {
            code = a + ((code - a + 13) % 26);
            let ch_new = char::from_u32(code).expect("Could not convert to char");
            chars.push(ch_new);
        } else if aa <= code && code <= zz {
            code = aa + ((code - aa + 13) % 26);
            let ch_new = char::from_u32(code).expect("Could not convert to char");
            chars.push(ch_new);
        } else {
            chars.push(ch);
        }
    }

    let other: String = chars.into_iter().collect();
    other
}

Convert vector of chars to String

  • iter

  • into_iter

  • The into_iter consumes the vector so we won't be able to use it again.

  • The iter allows for the reusing the iterator.

fn main() {
    let letters = vec!['R', 'u', 's', 't'];
    let name = letters.iter().collect::<String>();
    println!("{name}");

    let name = letters.into_iter().collect::<String>();
    println!("{name}");

    let text = String::from("The black cat");
    println!("{:?}", text.chars());
    let chars = text.chars().collect::<Vec<char>>();
    println!("{chars:?}");
    let text2 = text.chars().collect::<String>();
    assert_eq!(text, text2);

    let reversed: String = text.chars().rev().collect();
    println!("{text}");
    println!("{reversed}");
}
Rust
Rust
Chars(['T', 'h', 'e', ' ', 'b', 'l', 'a', 'c', 'k', ' ', 'c', 'a', 't'])
['T', 'h', 'e', ' ', 'b', 'l', 'a', 'c', 'k', ' ', 'c', 'a', 't']
The black cat
tac kcalb ehT

Vector of tuples

fn main() {
    let animals = vec![
        (String::from("elephant"), String::from("huge"), 100),
        (String::from("snake"), String::from("long"), 3),
    ];
    println!("{:?}", animals);
    for animal in &animals {
        println!("{} - {} - {}", animal.0, animal.1, animal.2);
    }

    // each field its own variable
    for (animal, size, weight) in &animals {
        println!("{animal} - {size} - {weight}");
    }

    // If we only need one of the values
    for (animal, _, _) in &animals {
        println!("{animal}");
    }
}
[("elephant", "huge", 100), ("snake", "long", 3)]
elephant - huge - 100
snake - long - 3
elephant - huge - 100
snake - long - 3
elephant
snake

Vector of structs

struct Animal<'a> {
    name: &'a str,
    size: &'a str,
    weight: i32,
}

fn main() {
    let animals = vec![
        Animal {
            name: "elephant",
            size: "huge",
            weight: 100,
        },
        Animal {
            name: "snake",
            size: "long",
            weight: 3,
        },
    ];

    for animal in &animals {
        println!("{} - {} - {}", animal.name, animal.size, animal.weight);
    }
}
elephant - huge - 100
snake - long - 3

Vector of structs - change value

struct Animal<'a> {
    name: &'a str,
    size: &'a str,
    weight: i32,
}

fn main() {
    let mut animals = vec![
        Animal {
            name: "elephant",
            size: "huge",
            weight: 100,
        },
        Animal {
            name: "snake",
            size: "long",
            weight: 3,
        },
    ];

    for animal in &animals {
        println!("{} - {} - {}", animal.name, animal.size, animal.weight);
    }

    animals[0].weight += 10;
    for animal in &animals {
        println!("{} - {} - {}", animal.name, animal.size, animal.weight);
    }
}
elephant - huge - 100
snake - long - 3
elephant - huge - 110
snake - long - 3

Join elements of vector into a string

  • join
fn main() {
    let names = vec!["Foo", "Bar", "Baz"];
    println!("{:?}", names);

    let joined = names.join("");
    println!("{}", joined);

    let csv = names.join(",");
    println!("{}", csv);
}
["Foo", "Bar", "Baz"]
FooBarBaz
Foo,Bar,Baz

Join vector of integers

  • join
  • iter
  • map
  • collect
fn main() {
    let integers = vec![1, 2, 3];
    println!("{:?}", integers);

    let joined = integers
        .iter()
        .map(|num| num.to_string())
        .collect::<Vec<_>>()
        .join(",");
    println!("{}", joined);
    assert_eq!(joined, "1,2,3");
}
[1, 2, 3]
1,2,3

Maximum value of a vector

  • max
  • match
  • iter
fn main() {
    let numbers = vec![10, 29, 3, 47, 8, 19];
    println!("{:?}", &numbers);
    let max = numbers.iter().max();
    println!("{:?}", max);

    match max {
        Some(&max_value) => println!("Maximum is {}", max_value),
        None => println!("The vector was empty!"),
    }

    let numbers: Vec<i32> = vec![];
    println!("{:?}", &numbers);
    let max = numbers.iter().max();
    println!("{:?}", max);

    match max {
        Some(&max_value) => println!("Maximum is {}", max_value),
        None => println!("The vector was empty!"),
    }
}
[10, 29, 3, 47, 8, 19]
Some(47)
Maximum is 47
[]
None
The vector was empty!

Longest or shortest string in a vector

  • max

  • min

  • max_by

  • min_by

  • cmp

  • max and min abc order

  • max and min by length

fn main() {
    let animals = ["snake", "mouse", "cat", "elephant"];
    println!("{:?}", animals);

    let max = animals.iter().max().unwrap();
    println!("{}", max);
    let min = animals.iter().min().unwrap();
    println!("{}", min);

    let longest = animals.iter().max_by(|x, y| x.len().cmp(&y.len())).unwrap();
    println!("{}", longest);

    let shortest = animals.iter().min_by(|x, y| x.len().cmp(&y.len())).unwrap();
    println!("{}", shortest);
}
["snake", "mouse", "cat", "elephant"]
snake
cat
elephant
cat

Change vector of numbers using map

  • into_iter
  • map
  • collect
fn main() {
    let numbers = vec![1, 2, 3];
    println!("{:?}", numbers);
    let doubles: Vec<i32> = numbers.into_iter().map(|x| x + 1).collect();

    // println!("{:?}", numbers); // borrow of moved value: `numbers`
    // Using iter() instead of into_iter() would let us use the old vector as well.
    println!("{:?}", doubles);
}
[1, 2, 3]
[2, 3, 4]

Update values in vector of structs using map

  • struct
struct Course {
    name: String,
}

fn main() {
    let courses: Vec<Course> = vec![
        Course {
            name: String::from("Programming Rust"),
        },
        Course {
            name: String::from("Git"),
        },
    ];
    for course in &courses {
        println!("{}", course.name);
    }

    let courses: Vec<Course> = courses
        .into_iter()
        .map(|mut course| {
            course.name.push_str(" and more");
            course
        })
        .collect();

    for course in &courses {
        println!("{}", course.name);
    }
}
Programming Rust
Git
Programming Rust and more
Git and more

map is lazy

In this example we can see that during the assignment to double nothing really happened. The iterator did not iterate. Only after the first println when we iterated over the elements of double, only then we saw the output from the print-statements inside the map.

fn main() {
    let numbers = vec![1, 3, 6];
    let double = numbers.into_iter().map(|num| {
        println!("doubling {num}");
        num * 2
    });

    println!("Nothing happended yet");
    for num in double {
        println!("{num:?}");
    }
}
Nothing happended yet
doubling 1
2
doubling 3
6
doubling 6
12

map is lazy that can cause problems

  • We increment the counter and that's how we get the increasing numbers next to the letters.
fn main() {
    let chars = vec!['a', 'b', 'c'];
    let mut cnt = 0;
    let pairs = chars.into_iter().map(|letter| {
        cnt += 1;
        (letter, cnt)
    });
    for pair in pairs {
        println!("{pair:?}");
    }
}
('a', 1)
('b', 2)
('c', 3)
  • If we call rev before we call map then the letters will be reversed, and the numbers will be attached to the letters after the reversal.
fn main() {
    let chars = vec!['a', 'b', 'c'];
    let mut c = 0;
    let pairs = chars.into_iter().rev().map(|letter| {
        c += 1;
        (letter, c)
    });
    for pair in pairs {
        println!("{pair:?}");
    }
}
('c', 1)
('b', 2)
('a', 3)
  • If we first call map and only then rev we would expect that first the numbers are generated and then the wholething is reversed, but that's not the case.
c 3
b 2
a 1
  • Because map is lazy it will be executed only after the reversal. Just as in the previous case.
// However this is also the same
fn main() {
    let chars = vec!['a', 'b', 'c'];
    let mut c = 0;
    let pairs = chars
        .into_iter()
        .map(|letter| {
            c += 1;
            (letter, c)
        })
        .rev();
    for pair in pairs {
        println!("{pair:?}");
    }
}
('c', 1)
('b', 2)
('a', 3)

filter numbers

  • iter

  • filter

  • cloned

  • collect

  • cloned

fn main() {
    let numbers: Vec<i32> = vec![-7, 0, 1, 2, 22, 23];
    println!("{:?}", &numbers);

    let same_numbers: Vec<i32> = numbers.into_iter().filter(|_number| true).collect();
    println!("{:?}", &same_numbers);

    // get vector of i32 references
    let positive_numbers = numbers
        .iter()
        .filter(|number| number.is_positive())
        .collect::<Vec<_>>();
    println!("{:?}", &positive_numbers);
    let expected: Vec<&i32> = vec![&1, &2, &22, &23];
    assert_eq!(positive_numbers, expected);

    // get vector of i32
    let positive_numbers = numbers
        .iter()
        .filter(|number| number.is_positive())
        .cloned()
        .collect::<Vec<_>>();
    println!("{:?}", &positive_numbers);
    let expected: Vec<i32> = vec![1, 2, 22, 23];
    assert_eq!(positive_numbers, expected);

    // get vector of i32 and move the numbers so we won't be able to use them again
    let big_numbers = numbers
        .into_iter()
        .filter(|number| number > &12)
        .collect::<Vec<_>>();
    println!("{:?}", &big_numbers);
    let expected: Vec<i32> = vec![22, 23];
    assert_eq!(big_numbers, expected);
}
[-7, 0, 1, 2, 22, 23]
[-7, 0, 1, 2, 22, 23]
[1, 2, 22, 23]
[1, 2, 22, 23]
[22, 23]

filter numbers iter into

  • iter_into
fn main() {
    let numbers: Vec<i32> = vec![1, 2, 3, 4, 5, 6, 7, 8];

    let even_number = numbers
        .into_iter()
        .filter(|n| n % 2 == 0)
        .collect::<Vec<_>>();

    println!("{:?}", even_number);
}
[2, 4, 6, 8]

filter numbers by named function

fn positive(number: &&i32) -> bool {
    number.is_positive()
}

fn main() {
    println!("{}", positive(&&3));
    println!("{}", positive(&&-3));
    let numbers: Vec<i32> = vec![-7, 0, 1, 2, 22, 23];
    println!("{:?}", &numbers);

    let positive_numbers: Vec<i32> = numbers
        .iter()
        .filter(|number| number.is_positive())
        .cloned()
        .collect();
    println!("{:?}", &positive_numbers);

    let positive_numbers: Vec<i32> = numbers.iter().filter(positive).cloned().collect();
    println!("{:?}", &positive_numbers);
}
true
false
[-7, 0, 1, 2, 22, 23]
[1, 2, 22, 23]
[1, 2, 22, 23]

filter string

  • iter
  • filter
  • cloned
  • collect
fn main() {
    let animals: Vec<String> = vec![
        String::from("elephant"),
        String::from("cat"),
        String::from("snake"),
        String::from("dog"),
    ];
    println!("{:?}", &animals);

    let same_animals: Vec<String> = animals.iter().filter(|_animal| true).cloned().collect();
    println!("{:?}", &same_animals);

    let short_animals: Vec<String> = animals
        .iter()
        .filter(|animal| animal.len() < 4)
        .cloned()
        .collect();
    println!("{:?}", &short_animals);
}
["elephant", "cat", "snake", "dog"]
["elephant", "cat", "snake", "dog"]
["cat", "dog"]

Two references to the same vector

  • TODO
#[derive(Debug)]
#[allow(dead_code)]
struct Something {
    number: i32,
    text: String,
    numbers: Vec<i32>,
}

fn main() {
    let va: Vec<Something> = vec![
        Something {
            number: 1,
            text: String::from("small"),
            numbers: vec![1, 2, 3],
        },
        Something {
            number: 11,
            text: String::from("medium"),
            numbers: vec![11, 12],
        },
        Something {
            number: 101,
            text: String::from("large"),
            numbers: vec![101],
        },
    ];
    println!("{:#?}", va);

    let vb = &va; // use & to borrow
    println!("{:#?}", vb);
    println!("{:#?}", va);
}
[
    Something {
        number: 1,
        text: "small",
        numbers: [
            1,
            2,
            3,
        ],
    },
    Something {
        number: 11,
        text: "medium",
        numbers: [
            11,
            12,
        ],
    },
    Something {
        number: 101,
        text: "large",
        numbers: [
            101,
        ],
    },
]
[
    Something {
        number: 1,
        text: "small",
        numbers: [
            1,
            2,
            3,
        ],
    },
    Something {
        number: 11,
        text: "medium",
        numbers: [
            11,
            12,
        ],
    },
    Something {
        number: 101,
        text: "large",
        numbers: [
            101,
        ],
    },
]
[
    Something {
        number: 1,
        text: "small",
        numbers: [
            1,
            2,
            3,
        ],
    },
    Something {
        number: 11,
        text: "medium",
        numbers: [
            11,
            12,
        ],
    },
    Something {
        number: 101,
        text: "large",
        numbers: [
            101,
        ],
    },
]

Filter vector of structs (cloning)

  • filter
  • cloned
  • Clone
  • TODO
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct Something {
    number: i32,
    text: String,
    numbers: Vec<i32>,
}

fn main() {
    let va: Vec<Something> = vec![
        Something {
            number: 1,
            text: String::from("small"),
            numbers: vec![1, 2, 3],
        },
        Something {
            number: 11,
            text: String::from("medium"),
            numbers: vec![11, 12],
        },
        Something {
            number: 101,
            text: String::from("large"),
            numbers: vec![101],
        },
    ];
    println!("{:#?}", &va);

    // This needs the Clone trait above and I am not sure if this does not mean that we duplicate
    // the data.
    let v_big = va
        .iter()
        .filter(|thing| thing.number > 20)
        .cloned()
        .collect::<Vec<Something>>();
    //let v_big = &va.into_iter().filter(|thing| thing.number > 20).collect::<Vec<Something>>();
    println!("{:#?}", &v_big);
    println!("{:#?}", &va);
}
[
    Something {
        number: 1,
        text: "small",
        numbers: [
            1,
            2,
            3,
        ],
    },
    Something {
        number: 11,
        text: "medium",
        numbers: [
            11,
            12,
        ],
    },
    Something {
        number: 101,
        text: "large",
        numbers: [
            101,
        ],
    },
]
[
    Something {
        number: 101,
        text: "large",
        numbers: [
            101,
        ],
    },
]
[
    Something {
        number: 1,
        text: "small",
        numbers: [
            1,
            2,
            3,
        ],
    },
    Something {
        number: 11,
        text: "medium",
        numbers: [
            11,
            12,
        ],
    },
    Something {
        number: 101,
        text: "large",
        numbers: [
            101,
        ],
    },
]

Convert vector of structs to vector of references

  • TODO
#[derive(Debug)]
#[allow(dead_code)]
struct Something {
    number: i32,
    text: String,
    numbers: Vec<i32>,
}

fn main() {
    let va: Vec<Something> = vec![
        Something {
            number: 1,
            text: String::from("small"),
            numbers: vec![1, 2, 3],
        },
        Something {
            number: 11,
            text: String::from("medium"),
            numbers: vec![11, 12],
        },
        Something {
            number: 101,
            text: String::from("large"),
            numbers: vec![101],
        },
    ];
    println!("{:#?}", va);

    //let vb = &va.iter().collect::<Vec<&Something>>();
    let vb = &va.iter().collect::<Vec<_>>();
    println!("{:#?}", vb);
    println!("{:#?}", va);
}
[
    Something {
        number: 1,
        text: "small",
        numbers: [
            1,
            2,
            3,
        ],
    },
    Something {
        number: 11,
        text: "medium",
        numbers: [
            11,
            12,
        ],
    },
    Something {
        number: 101,
        text: "large",
        numbers: [
            101,
        ],
    },
]
[
    Something {
        number: 1,
        text: "small",
        numbers: [
            1,
            2,
            3,
        ],
    },
    Something {
        number: 11,
        text: "medium",
        numbers: [
            11,
            12,
        ],
    },
    Something {
        number: 101,
        text: "large",
        numbers: [
            101,
        ],
    },
]
[
    Something {
        number: 1,
        text: "small",
        numbers: [
            1,
            2,
            3,
        ],
    },
    Something {
        number: 11,
        text: "medium",
        numbers: [
            11,
            12,
        ],
    },
    Something {
        number: 101,
        text: "large",
        numbers: [
            101,
        ],
    },
]

Filter vector of structs without copy

  • TODO
#[derive(Debug)]
#[allow(dead_code)]
struct Something {
    number: i32,
    text: String,
    numbers: Vec<i32>,
}

fn main() {
    let va: Vec<Something> = vec![
        Something {
            number: 1,
            text: String::from("small"),
            numbers: vec![1, 2, 3],
        },
        Something {
            number: 11,
            text: String::from("medium"),
            numbers: vec![11, 12],
        },
        Something {
            number: 101,
            text: String::from("large"),
            numbers: vec![101],
        },
    ];
    println!("{:#?}", va);

    //let vb = &va.iter().collect::<Vec<&Something>>();
    let vb = &va.iter().collect::<Vec<_>>();

    let v_big = &vb
        .iter()
        .filter(|thing| thing.number > 20)
        .collect::<Vec<_>>();
    //let v_big = &vb.into_iter().filter(|thing| thing.number > 20).collect::<Vec<_>>();
    println!("{:#?}", v_big);
    println!("{:#?}", vb);
}
[
    Something {
        number: 1,
        text: "small",
        numbers: [
            1,
            2,
            3,
        ],
    },
    Something {
        number: 11,
        text: "medium",
        numbers: [
            11,
            12,
        ],
    },
    Something {
        number: 101,
        text: "large",
        numbers: [
            101,
        ],
    },
]
[
    Something {
        number: 101,
        text: "large",
        numbers: [
            101,
        ],
    },
]
[
    Something {
        number: 1,
        text: "small",
        numbers: [
            1,
            2,
            3,
        ],
    },
    Something {
        number: 11,
        text: "medium",
        numbers: [
            11,
            12,
        ],
    },
    Something {
        number: 101,
        text: "large",
        numbers: [
            101,
        ],
    },
]

Combine filter and map into filter_map

  • filter
  • map
  • filter_map
fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let big_numbers = numbers.iter().filter(|x| **x > 5).collect::<Vec<_>>();
    let doubled_numbers = big_numbers.iter().map(|x| *x * 2).collect::<Vec<_>>();

    println!("numbers:         {numbers:?}");
    println!("big_numbers:     {big_numbers:?}");
    println!("doubled_numbers: {doubled_numbers:?}");

    let doubled_numbers = numbers
        .iter()
        .filter(|x| **x > 5)
        .map(|x| *x * 2)
        .collect::<Vec<_>>();
    println!("doubled_numbers: {doubled_numbers:?}");

    let doubled_numbers = numbers
        .iter()
        .filter_map(|x| if *x > 5 { Some(*x * 2) } else { None })
        .collect::<Vec<_>>();
    println!("doubled_numbers: {doubled_numbers:?}");
}
numbers:         [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
big_numbers:     [6, 7, 8, 9, 10]
doubled_numbers: [12, 14, 16, 18, 20]
doubled_numbers: [12, 14, 16, 18, 20]
doubled_numbers: [12, 14, 16, 18, 20]

Accessing the last element of a vector

  • len

  • last

  • TODO

  • Unlike Python and Perl, rust won't let us use a negative index in a vector so we won't be able to access the last element using vector_name[-1]

  • We can either use vector_name.len()-1 or

  • We can use vector_name.last(), but in this case we get an Option that can be None as well.

  • If we access a seemingly arbitrary element that we calculated using vector_name.len()-1 then either we get back a value or Rust will panic if we gave an index too big.

  • On the other hand using last we are more protected. In that case we either get a value or None if the vector was empty.

fn main() {
    let planets: Vec<&str> = vec!["Mercury", "Venus", "Earth"];
    println!("{}", planets.len());
    println!("{}", planets[planets.len() - 1]);
    // println!("{}", planets[ 5 ]);  // thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 5'
    println!("{}", planets.last().unwrap());

    let planets: Vec<&str> = vec![];
    //println!("{}", planets[ planets.len()-1 ]);  // thread 'main' panicked at 'attempt to subtract with overflow'
    println!("{:?}", planets.last()); // None
}
3
Earth
Earth
None

Insert element in vector

  • insert
  • TODO
//fn main() {
//    let text = "5678";
//    let rev: Vec<_> = text.chars().rev().collect();
//    //let rev: String = text.chars().rev().collect();
//    //println!("{rev}");
//    println!("{:?}", rev);
//}

fn main() {
    let mut values = vec!["1", "2", "3", "4"];
    println!("{:?}", values);
    values.insert(3, ",");
    println!("{:?}", values);
}

Vector with optional values - None or out of range?

  • get

  • is_none

  • is_some

  • TODO

  • If we have a vector that some of the elements can be None then the other elements must be Some-values and the whole thing must be defined using Option.

  • If we try to access an element in a vector that is out of range we get a run-time panic.

  • In order to avoid such panic we either need to check if our index is in range or we can use the get method.

  • We can use the get method to access the element. It will return None if the index was out of range.

  • Then the question arise, how do we know if the value was out of range or if it was in the range but the value was None?

fn main() {
    let numbers_real: Vec<Option<i32>> = vec![Some(3), None];
    println!("{:?}", numbers_real);

    println!("{:?}", numbers_real[1]); // None
    println!("{:?}", numbers_real.get(1)); // Some(None)
                                           // println!("{:?}", numbers_real[17]); // panic: index out of bounds: the len is 2 but the index is 17
    println!("{:?}", numbers_real.get(17)); // None    - out of range!
    println!();

    println!("{:?}", numbers_real.get(1).is_none());
    println!("{:?}", numbers_real.get(17).is_none()); // out of range!
    println!();

    println!("{:?}", numbers_real.get(1).is_some());
    println!("{:?}", numbers_real.get(17).is_some()); // out of range!
    println!();
}
[Some(3), None]
None
Some(None)
None

false
true

true
false

Vector with optional values

  • Option
  • None
  • Some
  • get
  • TODO
#[allow(clippy::needless_range_loop)]
#[allow(clippy::nonminimal_bool)]
fn main() {
    let numbers_real: Vec<Option<i32>> = vec![Some(3), None, None, Some(7), Some(9)];
    println!("{:?}", numbers_real);

    let numbers_ref: Vec<Option<&i32>> = vec![Some(&3), None, Some(&5), None, Some(&9)];
    println!("{:?}", numbers_ref);
    println!();

    for i in 0..numbers_real.len() {
        println!("{:?}", numbers_real[i]);
        println!("{:?}", numbers_ref[i]);
    }
    println!();

    for i in 0..numbers_real.len() {
        if !numbers_real[i].is_none() {
            println!("{}", numbers_real[i].unwrap());
        }
    }
    println!();

    for i in 0..numbers_real.len() {
        if !numbers_ref[i].is_none() && !numbers_real[i].is_none() {
            println!("{:?}", &numbers_real[i].unwrap() == numbers_ref[i].unwrap());
        }
    }
}
[Some(3), None, None, Some(7), Some(9)]
[Some(3), None, Some(5), None, Some(9)]

Some(3)
Some(3)
None
None
None
Some(5)
Some(7)
None
Some(9)
Some(9)

3
7
9

true
true

Vector length and capacity

  • len
  • capacity
  • with_capacity
  • resize
  • TODO
fn main() {
    // Create vector of 5 elements initialized to 0
    let vec = vec![0; 5];
    println!("{:?}", vec);

    // Create vector of 5 elements initialized to 42
    let vec = vec![42; 5];
    println!("{:?}", vec);

    // Creata an empty vector that can hold up to 5 elements.
    let mut vec: Vec<i32> = Vec::with_capacity(5);
    vec.resize(3, 19);
    println!("{:?}", vec);
    println!("len:      {:?}", vec.len());
    println!("capacity: {:?}", vec.capacity());
    println!();

    vec.resize(6, 23);
    println!("{:?}", vec);
    println!("len:      {:?}", vec.len());
    println!("capacity: {:?}", vec.capacity());
    println!();

    let mut vec: Vec<i32> = vec![];
    println!("{:?}", vec);
    println!("len:      {:?}", vec.len());
    println!("capacity: {:?}", vec.capacity());
    println!();

    vec.push(42);
    println!("{:?}", vec);
    println!("len:      {:?}", vec.len());
    println!("capacity: {:?}", vec.capacity());
    println!();
}
[0, 0, 0, 0, 0]
[42, 42, 42, 42, 42]
[19, 19, 19]
len:      3
capacity: 5

[19, 19, 19, 23, 23, 23]
len:      6
capacity: 10

[]
len:      0
capacity: 0

[42]
len:      1
capacity: 4

References to numbers

  • TODO
fn main() {
    let x = 2;
    let y = &2;
    println!("{}", x);
    println!("{}", y);

    println!("{}", &x == y);
    assert_eq!(&x, y);
}
2
2
true

Queue

  • VecDeque

  • push_back

  • pop_front

  • len

  • capacity

  • VecDeque provides for a fast queue

  • It probably has to be mutable to make sense though we could create one from a fixed list of values and then just access the elements.

  • We can add element at the end using push_back.

  • We can fetch elements from the front using pop_front.

  • As we add more elements Rust will automatically grow the capacity of the vector by a little bit more than needed to allow for faster growth.

use std::collections::VecDeque;

fn main() {
    let mut doctor = VecDeque::new();
    println!(
        "len: {} capacity: {} empty:  {}",
        doctor.len(),
        doctor.capacity(),
        doctor.is_empty()
    );

    doctor.push_back(String::from("cat"));
    println!(
        "len: {} capacity: {} empty:  {}",
        doctor.len(),
        doctor.capacity(),
        doctor.is_empty()
    );

    doctor.push_back(String::from("dog"));
    doctor.push_back(String::from("rat"));
    doctor.push_back(String::from("squirrel"));
    doctor.push_back(String::from("snake"));
    println!(
        "len: {} capacity: {} empty:  {}",
        doctor.len(),
        doctor.capacity(),
        doctor.is_empty()
    );

    println!("{:?}", doctor); // I can print the whole queue, but there is probably no point for that

    let next = doctor.pop_front();
    println!("{}", next.unwrap());
    println!(
        "len: {} capacity: {} empty:  {}",
        doctor.len(),
        doctor.capacity(),
        doctor.is_empty()
    );
}
len: 0 capacity: 0 empty:  true
len: 1 capacity: 4 empty:  false
len: 5 capacity: 8 empty:  false
["cat", "dog", "rat", "squirrel", "snake"]
cat
len: 4 capacity: 8 empty:  false

Iterate over both index and value of a vector (enumerate)

  • for

  • iter

  • enumerate

  • Instead of getting the index of the current element of Rust, we can either iteratore over the indices or use enumerate.

  • First example: iterating over the values.

  • Second example: iterating over the indices and getting the value. This triggers a needless_range_loop suggesting the third solution:

  • Third example: Creating an iterator out of the vector and calling enumerate on it. This will allow us to iterate over the index-value pairs.

fn main() {
    let animals = vec!["cat", "snake", "camel"];
    for animal in &animals {
        println!("{animal}");
    }
    println!();

    #[allow(clippy::needless_range_loop)]
    for ix in 0..animals.len() {
        println!("{ix} {}", animals[ix]);
    }
    println!();

    for (ix, animal) in animals.iter().enumerate() {
        println!("{ix} {animal}");
    }
}
cat
snake
camel

0 cat
1 snake
2 camel

0 cat
1 snake
2 camel

Create vector of strings from array of str using from_iter

  • from_iter
fn main() {
    let animals = Vec::from_iter(
        ["mouse", "elephant", "cat", "dog", "giraffe"].map(|animal| animal.to_owned()),
    );

    println!("animals: {:?}", animals);
}
animals: ["mouse", "elephant", "cat", "dog", "giraffe"]

Memory allocation of vector of numbers

  • extend
  • as_ptr
macro_rules! prt {
    ($var: expr) => {
        println!(
            "{:2} {:2} {:p} {:?} {:?}",
            $var.len(),
            $var.capacity(),
            &$var,
            $var.as_ptr(),
            $var
        );
    };
}

macro_rules! prtn {
    ($var: expr) => {
        println!(
            "{:2} {:2} {:p} {:?} {:?}",
            $var.len(),
            $var.capacity(),
            &$var,
            $var.as_ptr(),
            $var
        );
        for ix in 0..$var.len() {
            println!("{:2}    {:p} {:?}", ix, &$var[ix], $var[ix]);
        }
    };
}
fn main() {
    let mut numbers: Vec<i32> = vec![19, 23];

    prtn!(numbers);
    println!();
    let x = String::from("hi");
    prt!(x);

    numbers.extend([1, 2, 3, 4]);
    prtn!(numbers);
    println!();

    numbers.extend([5]);
    prtn!(numbers);
    println!();
}
 2  2 0x7ffcee29d998 0x5fc29ca5db80 [19, 23]
 0    0x5fc29ca5db80 19
 1    0x5fc29ca5db84 23

 2  2 0x7ffcee29e060 0x5fc29ca5dba0 "hi"
 6  6 0x7ffcee29d998 0x5fc29ca5db80 [19, 23, 1, 2, 3, 4]
 0    0x5fc29ca5db80 19
 1    0x5fc29ca5db84 23
 2    0x5fc29ca5db88 1
 3    0x5fc29ca5db8c 2
 4    0x5fc29ca5db90 3
 5    0x5fc29ca5db94 4

 7 12 0x7ffcee29d998 0x5fc29ca5dbc0 [19, 23, 1, 2, 3, 4, 5]
 0    0x5fc29ca5dbc0 19
 1    0x5fc29ca5dbc4 23
 2    0x5fc29ca5dbc8 1
 3    0x5fc29ca5dbcc 2
 4    0x5fc29ca5dbd0 3
 5    0x5fc29ca5dbd4 4
 6    0x5fc29ca5dbd8 5

  • The Vec contains 3 values: len, capcatity and a pointer (ptr) to where the actual vector is. This is on the stack.
  • The actual vector is on the heap.
  • In case of integers the values of the vector are in a continuous block of memory starting from the place where the ptr points to.
  • In this case we have i32 values in the vector. Each one takes up 4 bytes starting from 0x5fc29ca5db80.
  • We created another variable on the heap that was placed on 0x5fc29ca5dba0. This is 32 bytes higher.
  • As we extend the vector with 4 more numbers to have 6 elements, we can see that the vector remains in the same place. 0x5fc29ca5db80.
  • When we extend it to 7 elements taking up 7*4 = 28 bytes, we can see the vector was moved to the memory starting at 0x5fc29ca5dbc0.
  • It is unclear to me why was it already moved while we still could have added one more i32.

Memory allocation of vector of strings

macro_rules! prt {
    ($var: expr) => {
        println!(
            "{:2} {:2} {:p} {:?} {:?}",
            $var.len(),
            $var.capacity(),
            &$var,
            $var.as_ptr(),
            $var
        );
    };
}
fn main() {
    let mut animals = vec![String::from("cat"), String::from("dog")];

    prt!(animals);
    prt!(animals[0]);
    prt!(animals[1]);
    println!();

    animals.extend([String::from("mouse")]);

    prt!(animals);
    prt!(animals[0]);
    prt!(animals[1]);
    prt!(animals[2]);
    println!();

    animals[0].push_str(" is the most dangerous animal");
    prt!(animals);
    prt!(animals[0]);
    prt!(animals[1]);
    prt!(animals[2]);
}
 2  2 0x7ffeaaaa5a98 0x5e44f7653b80 ["cat", "dog"]
 3  3 0x5e44f7653b80 0x5e44f7653bc0 "cat"
 3  3 0x5e44f7653b98 0x5e44f7653be0 "dog"

 3  4 0x7ffeaaaa5a98 0x5e44f7653c20 ["cat", "dog", "mouse"]
 3  3 0x5e44f7653c20 0x5e44f7653bc0 "cat"
 3  3 0x5e44f7653c38 0x5e44f7653be0 "dog"
 5  5 0x5e44f7653c50 0x5e44f7653c00 "mouse"

 3  4 0x7ffeaaaa5a98 0x5e44f7653c20 ["cat is the most dangerous animal", "dog", "mouse"]
32 32 0x5e44f7653c20 0x5e44f7653c90 "cat is the most dangerous animal"
 3  3 0x5e44f7653c38 0x5e44f7653be0 "dog"
 5  5 0x5e44f7653c50 0x5e44f7653c00 "mouse"
  • Each string

Exercise: Count words using two vectors

  • Given a string that consists of words and white-spaces, count how many times each word appears!
  • In this solution we use two vectors. A much better solution would be to use HashMap, but in this example I wanted to show the solution with two vectors.
  • One vector will hold the list of distinct words.
  • The second vector will hold the count for each word.

Solution: Count words using two vectors

fn main() {
    let text = String::from("mouse cat cat oliphant");
    let parts = text.split_whitespace();

    let mut words: Vec<&str> = vec![];
    let mut count: Vec<i32> = vec![];

    for word in parts {
        let mut found = false;
        for ix in 0..words.len() {
            if words[ix] == word {
                count[ix] += 1;
                found = true;
                break;
            }
        }
        if !found {
            words.push(word);
            count.push(1);
        }
    }

    // report
    for ix in 0..words.len() {
        println!("{}: {}", words[ix], count[ix])
    }
}
mouse: 1
cat: 2
oliphant: 1

HashMap in Rust

What is a HashMap?

  • A data structure holding key-value pairs with an O(1) access time.

  • Hash (Perl)

  • Map (Java)

  • Dictionary (Python)

  • Associative Array (PHP)

Create empty HashMap, insert key-value pairs

  • HashMap

  • new

  • insert

  • len

  • mut

  • std::collections

  • HashMap

  • When we create a HashMap we don't necessarily have to define the types of the keys and the values as those can be deducted from the later assignments.

  • If we'd like to add new key-value pairs to the hash, we need to declare it as mutable.

  • The insert method allows us to add a new key-value pair.

  • len will tell us the number of keys.

use std::collections::HashMap;

fn main() {
    let mut counter = HashMap::new();
    println!("{}", counter.len());
    println!("{:?}", counter);

    counter.insert("foo", 1);
    counter.insert("bar", 2);
    println!("{:?}", counter.len());
    println!("{:?}", counter);
}
0
{}
2
{"foo": 1, "bar": 2}

Create immutable hash with data

  • HashMap

  • from

  • keys

  • We can also create a HashMap from existing data. In this case the hash does not have to be mutable.

use std::collections::HashMap;

fn main() {
    let counter = HashMap::from([("foo", 1), ("bar", 2)]);
    println!("{:?}", counter);
    println!("{:?}", counter.keys());

    // counter.insert("other", 3); // cannot borrow `counter` as mutable, as it is not declared as mutable
}
{"foo": 1, "bar": 2}
["foo", "bar"]

Check if hash contains key (key exists)

  • from
  • contains_key
  • contains
  • exists
use std::collections::HashMap;

fn main() {
    let counter = HashMap::from([("foo", 1), ("bar", 2)]);
    println!("{:?}", counter.contains_key("zz"));
    println!("{:?}", counter.contains_key("foo"));
}
false
true

Get value from hash

  • get

  • from

  • get returns an Option containing the value corresponding to the key, or None, if the key does not exist.

use std::collections::HashMap;

fn main() {
    let counter = HashMap::from([("foo", 1), ("bar", 2)]);
    println!("{}", counter["foo"]);
    println!("{:?}", counter.get("foo"));

    // println!("{}", counter["zz"]); // panic
    println!("{:?}", counter.get("zz")); // None

    println!();

    match counter.get("foo") {
        Some(val) => println!("{val}"),
        None => println!("None"),
    };

    match counter.get("zz") {
        Some(val) => println!("{val}"),
        None => println!("None"),
    };
}
1
Some(1)
None

1
None

Iterate over keys of a hash

  • keys
  • from
use std::collections::HashMap;

fn main() {
    let counter = HashMap::from([("foo", 1), ("bar", 2)]);
    println!("{:?}", counter);
    println!("{:?}", counter.keys());

    for name in counter.keys() {
        println!("{} : {}", name, counter[name]);
    }
}
{"bar": 2, "foo": 1}
["bar", "foo"]
bar : 2
foo : 1

Iterate over key-value pairs in a Hash

  • keys

  • iter

  • Use the iter method to get the iterator

  • Though you can iterate over the hash directly. It does the same.

use std::collections::HashMap;

fn main() {
    let counter = HashMap::from([("foo", 1), ("bar", 2)]);
    println!("{:?}", counter);
    println!("{:?}", counter.keys());

    for (name, value) in counter.iter() {
        println!("{} : {}", name, value);
    }
    println!();

    for (name, value) in counter {
        println!("{} : {}", name, value);
    }
}
{"bar": 2, "foo": 1}
["bar", "foo"]
bar : 2
foo : 1

bar : 2
foo : 1

Rust hash update value

  • entry

  • new

  • or_insert

  • Using the or_insert method we can update a value and insert a default value in case the key did not exist.

use std::collections::HashMap;

fn main() {
    let mut counter = HashMap::new();
    *counter.entry("apple").or_insert(0) += 1;
    println!("{:#?}", counter);

    *counter.entry("banana").or_insert(0) += 1;
    println!("{:#?}", counter);

    *counter.entry("apple").or_insert(0) += 1;
    println!("{:#?}", counter);
}
{
    "apple": 1,
}
{
    "banana": 1,
    "apple": 1,
}
{
    "banana": 1,
    "apple": 2,
}
use std::collections::HashMap;

fn main() {
    let mut grades = HashMap::new();

    grades.insert(String::from("Joe"), 70); // create
    println!("{}", grades["Joe"]);

    grades.insert(String::from("Joe"), 71); // overwite
    println!("{}", grades["Joe"]);

    grades.entry(String::from("Jane")).or_insert(80); // inserts if the key did not exist
    println!("{}", grades["Jane"]);

    grades.entry(String::from("Jane")).or_insert(81); // does not change the value
    println!("{}", grades["Jane"]);

    // inplace change of value (defaults to 0 if key does not exist)
    let value = grades.entry(String::from("Joe")).or_insert(0);
    *value += 1;
    println!("{}", grades["Joe"]);

    // the same, no need for temporary variable
    *grades.entry(String::from("Joe")).or_insert(0) += 1;
    println!("{}", grades["Joe"]);

    // This did not exists so defaults to 0
    *grades.entry(String::from("George")).or_insert(0) += 1;
    println!("{}", grades["George"]);
}

Rust update values in a hash - count words

  • entry
  • or_insert}
use std::collections::HashMap;

fn main() {
    let fruits = ["apple", "banana", "peach", "apple"];
    let mut counter = HashMap::new();

    for fruit in fruits {
        println!("{}", fruit);
        *counter.entry(fruit).or_insert(0) += 1;
    }
    println!("{:#?}", counter);
}

Remove element from hash

  • remove
  • keys
use std::collections::HashMap;

fn main() {
    let mut counter = HashMap::from([("foo", 1), ("bar", 2)]);
    println!("{:?}", counter);
    println!("{:?}", counter.keys());
    let was = counter.remove("foo");
    println!("{:?}", was);
    println!("{:?}", counter);
    println!("{:?}", counter.keys());
}
{"bar": 2, "foo": 1}
["bar", "foo"]
Some(1)
{"bar": 2}
["bar"]

Accessing values

  • unsert

  • unwrap_or

  • get

  • copied

  • unwrap_or(0) sets a default value to 0

use std::collections::HashMap;

fn main() {
    let mut grades = HashMap::new();

    grades.insert(String::from("Joe"), 70);
    grades.insert(String::from("Jane"), 80);

    let name = String::from("Joe");
    let score = grades.get(&name);
    println!("{:?}", score);

    let score = grades[&name];
    println!("{}", score);

    let score = grades.get(&name).copied().unwrap_or(0);
    println!("{}", score);

    let name = String::from("George");
    let score = grades.get(&name).copied().unwrap_or(0);
    println!("{}", score);
}
Some(70)
70
70
0

Split string create hash

  • split_whitespace
  • collect
  • insert
use std::collections::HashMap;

fn main() {
    let text = String::from("apple   banana");
    println!("The original '{text}'");
    let mut data = HashMap::new();

    //let parts = text.split(" ");
    let parts = text.split_whitespace(); // return an iterator
                                         //println!("{:?}", parts);
                                         //for part in parts {
                                         //    println!("'{}'", part);
                                         //}
                                         //
    let parts: Vec<&str> = parts.collect(); // collect the items from an iterator to be a vector
    println!("parts: {:?}", parts);
    println!("First element '{}'", parts[0]);

    for part in parts {
        data.insert(part, part);
    }
    println!("The Hash: {:?}", data);
}
The original 'apple   banana'
parts: ["apple", "banana"]
First element 'apple'
The Hash: {"apple": "apple", "banana": "banana"}

Create hash from key-value pairs after split

  • to_string
  • collect
  • insert
use std::collections::HashMap;

fn main() {
    let input: Vec<String> = vec![String::from("Monday=1"), String::from("Tuesday=2")];

    let mut data = HashMap::new();
    for entry in input {
        let parts = mysplit(&entry);

        data.insert(parts[0].to_owned(), parts[1].to_owned());
    }
    println!("{:?}", data);
}

fn mysplit(entry: &str) -> Vec<&str> {
    let parts = entry.split('=');
    let parts: Vec<&str> = parts.collect();
    parts
}
{"Tuesday": "2", "Monday": "1"}

Read key-value pairs from file

elephant=big
snake=long
cheetah=fast
Ant=small
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};

fn main() {
    let filename = "key_value_pairs.txt";
    let mut data = HashMap::new();
    match File::open(filename) {
        Ok(file) => {
            let reader = BufReader::new(file);
            for line in reader.lines() {
                let line = line.unwrap();
                // println!("'{}'", line);
                let parts = line.split('=');
                //println!("{:?}", parts);
                let parts: Vec<&str> = parts.collect();
                // println!("{:?}", parts);
                //println!("{:?}", parts[0]);
                data.insert(parts[0].to_owned(), parts[1].to_owned());
            }
        }
        Err(error) => {
            println!("Error opening file {}: {}", filename, error);
        }
    }
    println!("{:?}", data);
}
{"snake": "long", "elephant": "big", "Ant": "small", "cheetah": "fast"}

Sort vector of hashes

  • sort_by
  • push
use std::collections::HashMap;

fn main() {
    let joe = HashMap::from([("name", "Joe"), ("birthyear", "1993")]);
    println!("{:?}", joe);

    let mary = HashMap::from([("name", "Mary"), ("birthyear", "1994")]);
    println!("{:?}", mary);

    let lue = HashMap::from([("name", "Lue"), ("birthyear", "1992")]);
    println!("{:?}", lue);

    let mut people: Vec<_> = vec![joe];
    people.push(mary);
    people.push(lue);

    println!("{:?}", people);

    people.sort_by(|a, b| a["birthyear"].cmp(b["birthyear"]));
    println!("{:?}", people);
}
{"birthyear": "1993", "name": "Joe"}
{"name": "Mary", "birthyear": "1994"}
{"birthyear": "1992", "name": "Lue"}
[{"birthyear": "1993", "name": "Joe"}, {"name": "Mary", "birthyear": "1994"}, {"birthyear": "1992", "name": "Lue"}]
[{"birthyear": "1992", "name": "Lue"}, {"birthyear": "1993", "name": "Joe"}, {"name": "Mary", "birthyear": "1994"}]

Mapping structs based on more than one key

  • Debug
  • dead_code
  • struct
  • HashMap
use std::collections::HashMap;

#[derive(Debug)]
#[allow(dead_code)]
struct Something {
    key1: String,
    key2: String,
}

fn main() {
    let a = Something {
        key1: String::from("one"),
        key2: String::from("two"),
    };
    let b = Something {
        key1: String::from("uno"),
        key2: String::from("dos"),
    };
    dbg!(&a);
    dbg!(&b);

    let mut mapping_from_key1: HashMap<String, &Something> = HashMap::new();
    mapping_from_key1.insert(a.key1.clone(), &a);
    mapping_from_key1.insert(b.key1.clone(), &b);

    let mut mapping_from_key2: HashMap<String, &Something> = HashMap::new();
    mapping_from_key2.insert(a.key2.clone(), &a);
    mapping_from_key2.insert(b.key2.clone(), &b);

    dbg!(&mapping_from_key1);
    dbg!(&mapping_from_key2);
}

Mapping structs based on more than one key from a vector

use std::collections::HashMap;

#[derive(Debug)]
#[allow(dead_code)]
struct Something {
    key1: String,
    key2: String,
}

fn main() {
    let things: Vec<Something> = vec![
        Something {
            key1: String::from("one"),
            key2: String::from("two"),
        },
        Something {
            key1: String::from("uno"),
            key2: String::from("dos"),
        },
    ];
    dbg!(&things);

    let mut mapping_from_key1: HashMap<String, &Something> = HashMap::new();
    let mut mapping_from_key2: HashMap<String, &Something> = HashMap::new();
    for thing in &things {
        mapping_from_key1.insert(thing.key1.clone(), thing);
        mapping_from_key2.insert(thing.key2.clone(), thing);
    }

    dbg!(&mapping_from_key1);
    dbg!(&mapping_from_key2);
}

Create hash from vector of tuple pairs

  • vec!
  • Vec
  • HashMap
  • from_iter
use std::collections::HashMap;

fn main() {
    let mut pairs: Vec<(String, i32)> = vec![(String::from("snake"), 1), (String::from("dog"), 2)];
    let cat = (String::from("cat"), 3);
    pairs.push(cat);
    println!("vector of pairs: {:?}", pairs);

    let counter: HashMap<String, i32> = HashMap::from_iter(pairs);
    println!("hash: {:?}", counter);
    println!("dog: {:?}", counter["dog"]);
}
vector of pairs: [("snake", 1), ("dog", 2), ("cat", 3)]
hash: {"cat": 3, "snake": 1, "dog": 2}
dog: 2

Hash of vectors in Rust

  • and_modify
  • split
  • contains_key
  • insert
  • or_insert
  • push
use std::collections::HashMap;

#[derive(Debug, PartialEq)]
#[allow(dead_code)]
struct Address {
    atype: String,
    phone: String,
}

fn main() {
    let mut addresses: HashMap<String, Vec<Address>> = HashMap::new();

    for text in [
        String::from("Foo Bar,home,+03-1234567"),
        String::from("Joe Doe,mobile,+1-1234-567"),
        String::from("Foo Bar,mobile,+42-1234567"),
    ] {
        let parts = text.split(',').collect::<Vec<&str>>();
        let name = parts[0].to_owned();
        let atype = parts[1].to_owned();
        let phone = parts[2].to_owned();

        let address = Address { atype, phone };

        if !addresses.contains_key(&name) {
            addresses.insert(name.clone(), vec![]);
        }
        addresses
            .entry(name)
            .and_modify(|value| value.push(address));

        // instead ot the above 4 lines we could also write:
        // addresses.entry(String::from(name)).or_insert(vec![]).push(address);
    }

    println!("{:#?}", addresses);
    let expected = HashMap::from([
        (
            String::from("Foo Bar"),
            vec![
                Address {
                    atype: String::from("home"),
                    phone: String::from("+03-1234567"),
                },
                Address {
                    atype: String::from("mobile"),
                    phone: String::from("+42-1234567"),
                },
            ],
        ),
        (
            String::from("Joe Doe"),
            vec![Address {
                atype: String::from("mobile"),
                phone: String::from("+1-1234-567"),
            }],
        ),
    ]);

    assert_eq!(addresses, expected);
}
{
    "Foo Bar": [
        Address {
            atype: "home",
            phone: "+03-1234567",
        },
        Address {
            atype: "mobile",
            phone: "+42-1234567",
        },
    ],
    "Joe Doe": [
        Address {
            atype: "mobile",
            phone: "+1-1234-567",
        },
    ],
}

Add items to a hash of vectors using a function

  • split
  • or_insert
  • push
use std::collections::HashMap;

#[derive(Debug, PartialEq)]
#[allow(dead_code)]
struct Address {
    atype: String,
    phone: String,
}

fn main() {
    let mut addresses: HashMap<String, Vec<Address>> = HashMap::new();

    for text in [
        String::from("Foo Bar,home,+03-1234567"),
        String::from("Joe Doe,mobile,+1-1234-567"),
        String::from("Foo Bar,mobile,+42-1234567"),
    ] {
        add_address(&mut addresses, text);
    }

    println!("{:#?}", addresses);
    let expected = HashMap::from([
        (
            String::from("Foo Bar"),
            vec![
                Address {
                    atype: String::from("home"),
                    phone: String::from("+03-1234567"),
                },
                Address {
                    atype: String::from("mobile"),
                    phone: String::from("+42-1234567"),
                },
            ],
        ),
        (
            String::from("Joe Doe"),
            vec![Address {
                atype: String::from("mobile"),
                phone: String::from("+1-1234-567"),
            }],
        ),
    ]);

    assert_eq!(addresses, expected);
}

fn add_address(addresses: &mut HashMap<String, Vec<Address>>, text: String) {
    let parts: Vec<&str> = text.split(',').collect();
    let name = parts[0];
    let atype = parts[1];
    let phone = parts[2];

    let address = Address {
        atype: String::from(atype),
        phone: String::from(phone),
    };

    addresses
        .entry(String::from(name))
        .or_default()
        .push(address);
}
{
    "Foo Bar": [
        Address {
            atype: "home",
            phone: "+03-1234567",
        },
        Address {
            atype: "mobile",
            phone: "+42-1234567",
        },
    ],
    "Joe Doe": [
        Address {
            atype: "mobile",
            phone: "+1-1234-567",
        },
    ],
}

Integers as keys of a HashMap

use std::collections::HashMap;

fn main() {
    let mut lookup = HashMap::new();
    lookup.insert(0, "Sunday");
    lookup.insert(1, "Monday");
    lookup.insert(2, "Tuesday");

    println!("{:?}", lookup);
}
{0: "Sunday", 1: "Monday", 2: "Tuesday"}

Tuples as keys of a HashMap

use std::collections::HashMap;

fn main() {
    let mut lookup = HashMap::new();
    let a = ("Foo", 2);
    lookup.insert(a, 4);

    lookup.insert(("Bar", 4), 6);

    // The key must be of the same type
    // lookup.insert(("Other", 5, 6), 7);

    println!("{:?}", lookup);
}
{("Foo", 2): 4, ("Bar", 4): 6}

Structs as keys of a HashMap

  • struct

  • derive

  • Debug

  • Hash

  • Eq

  • PartialEq

  • We can also use structs as a keys in a HashMap, but for that we need to add a few traits.

use std::collections::HashMap;

#[derive(Debug, Hash, Eq, PartialEq)]
struct Person {
    fname: String,
    lname: String,
}

fn main() {
    let a = Person {
        fname: String::from("Steve"),
        lname: String::from("Jobs"),
    };

    let m = Person {
        fname: String::from("Bill"),
        lname: String::from("Gates"),
    };

    let mut lookup = HashMap::new();
    lookup.insert(a, "Apple");
    lookup.insert(m, "Microsoft");

    println!("{:?}", lookup);
}
{Person { fname: "Steve", lname: "Jobs" }: "Apple", Person { fname: "Bill", lname: "Gates" }: "Microsoft"}

Merge HashMaps (extend)

  • extend

  • If the same key appears twice, the value of the second one wins.

use std::collections::HashMap;
fn main() {
    let a = HashMap::from([("apple", 1), ("banana", 1)]);
    let b = HashMap::from([("apple", 2), ("peach", 2), ("grape", 2)]);

    let mut total: HashMap<&str, i32> = HashMap::new();
    total.extend(a);
    println!("{:#?}", total);

    total.extend(b);
    println!("{:#?}", total);
}
{
    "apple": 1,
    "banana": 1,
}
{
    "peach": 2,
    "banana": 1,
    "grape": 2,
    "apple": 2,
}

Merge two HashMaps adding the values

  • HashMap

Given

{
    "a" : 1,
    "b" : 2,
}

{
    "a" : 3,
    "c" : 5,
}

Adding them together should result in

{
    "a" : 4,
    "b" : 2,
    "c" : 5,
}
use std::collections::HashMap;
fn main() {
    let a = HashMap::from([("apple", 1), ("banana", 1)]);
    let b = HashMap::from([("apple", 2), ("peach", 2), ("grape", 2)]);

    let mut total: HashMap<&str, i32> = HashMap::new();

    for (key, value) in a.iter() {
        *total.entry(key).or_insert(0) += value;
    }
    println!("{:#?}", total);
    assert_eq!(total, a);

    for (key, value) in b.iter() {
        *total.entry(key).or_insert(0) += value;
    }
    println!("{:#?}", total);

    let expected_total = HashMap::from([("apple", 3), ("peach", 2), ("grape", 2), ("banana", 1)]);
    assert_eq!(total, expected_total);
}
{
    "banana": 1,
    "apple": 1,
}
{
    "banana": 1,
    "peach": 2,
    "grape": 2,
    "apple": 3,
}

Merge two HashMaps adding the values implemented as a function

  • HashMap
use std::collections::HashMap;
fn main() {
    let a = HashMap::from([("apple", 1), ("banana", 1)]);
    let b = HashMap::from([("apple", 2), ("peach", 2), ("grape", 2)]);

    let mut total: HashMap<&str, i32> = HashMap::new();

    add(&mut total, &a);
    println!("{:#?}", total);
    assert_eq!(total, a);

    add(&mut total, &b);
    println!("{:#?}", total);

    let expected_total = HashMap::from([("apple", 3), ("peach", 2), ("grape", 2), ("banana", 1)]);
    assert_eq!(total, expected_total);
}

fn add<'a>(total: &mut HashMap<&'a str, i32>, data: &HashMap<&'a str, i32>) {
    for (key, value) in data.iter() {
        *total.entry(key).or_insert(0) += value;
    }
}
{
    "banana": 1,
    "apple": 1,
}
{
    "banana": 1,
    "peach": 2,
    "grape": 2,
    "apple": 3,
}

Merge two HashMaps adding the values in a function

  • HashMap
use std::collections::HashMap;
fn main() {
    let a = HashMap::from([("apple", 1), ("banana", 1)]);
    let b = HashMap::from([("apple", 2), ("peach", 2), ("grape", 2)]);

    let mut total: HashMap<&str, i32> = HashMap::new();

    add(&mut total, &a);
    println!("{:#?}", total);

    add(&mut total, &b);
    println!("{:#?}", total);
}

fn add<'a>(total: &mut HashMap<&'a str, i32>, other: &HashMap<&'a str, i32>) {
    for (key, value) in other.iter() {
        *total.entry(key).or_insert(0) += value;
    }
}

// fn add(total: &mut HashMap<&str, i32>, other : &HashMap<&str, i32>) {
//     for (key, value) in other.iter() {
//         *total.entry(key).or_insert(0) += value;
//     }
// }
{
    "banana": 1,
    "apple": 1,
}
{
    "banana": 1,
    "peach": 2,
    "grape": 2,
    "apple": 3,
}

Convert HashMap to vector of tuples and sort by key or by value

use std::collections::HashMap;


fn main() {
    let animals = HashMap::from([
        ("cat", 2),
        ("dog", 3),
        ("snake", 4),
        ("crab", 5),
    ]);
    println!("animals: {animals:?}");

    for pair in &animals {
        println!("{pair:?}");
    }

    let mut animals = animals.iter().collect::<Vec<_>>();
    animals.sort();
    println!("animals: {animals:?}");

    animals.sort_by_key(|entry| entry.1);
    println!("animals: {animals:?}");

}
animals: {"dog": 3, "snake": 4, "cat": 2, "crab": 5}
("dog", 3)
("snake", 4)
("cat", 2)
("crab", 5)
animals: [("cat", 2), ("crab", 5), ("dog", 3), ("snake", 4)]
animals: [("cat", 2), ("dog", 3), ("snake", 4), ("crab", 5)]

Other: Add method to HashMap that sums the values

  • HashMap
  • TODO
use std::collections::HashMap;

pub trait SumValues<'a> {
    fn add(&mut self, other: &HashMap<&'a str, i32>);
}

impl<'a> SumValues<'a> for HashMap<&'a str, i32> {
    fn add(&mut self, other: &HashMap<&'a str, i32>) {
        // println!("self: {self:?}");
        // println!("other: {other:?}");
        for (key, value) in other.iter() {
            *self.entry(key).or_insert(0) += value;
        }
        // println!("self: {self:?}");
    }
}

fn main() {
    let a = HashMap::from([("apple", 1), ("banana", 1)]);
    let b = HashMap::from([("apple", 2), ("peach", 2), ("grape", 2)]);

    let mut total: HashMap<&str, i32> = HashMap::new();

    total.add(&a);
    println!("{:#?}", total);
    assert_eq!(total, a);

    total.add(&b);
    println!("{:#?}", total);
    assert_eq!(
        total,
        HashMap::from([("apple", 3), ("peach", 2), ("grape", 2), ("banana", 1),])
    );
}
{
    "apple": 1,
    "banana": 1,
}
{
    "apple": 3,
    "banana": 1,
    "grape": 2,
    "peach": 2,
}

Enums

Why enums

  • Defining an enum

  • An enum type has variants.

  • match pattern matching operations must be exhaustive, this helps us ensuring that we handled every variant.

Enum to represent exit status

  • We define a type called ExitCode that has two variants: Success and Failure.

  • We can then, based on the results of our porgram set the value of exit.

  • However, this is a bit limited as we can only indicate failure without the details we are used to - which is a number.

  • By default Rust does not implement the Debug trait for an arbitrary enum so we derive from the Debug trait to be able to print the values using the :? placeholder.

  • We can also observe that if-statements can have a return value.

#[derive(Debug)]
enum ExitCode {
    Success,
    Failure,
}

fn main() {
    let args = std::env::args().collect::<Vec<_>>();
    let exit = if args.len() > 1 {
        ExitCode::Success
    } else {
        ExitCode::Failure
    };

    println!("{exit:?}");
}

Enum to represent exit code

  • We can also define Failure variant of the ExitCode type to have an number - a small number holding a value between 0-255.
  • We can use a match statement to extract the actual number from the Failure.
#![allow(clippy::comparison_chain)]

#[derive(Debug)]
enum ExitCode {
    Success,
    Failure(u8),
}

fn main() {
    let args = std::env::args().collect::<Vec<_>>();
    let exit = if args.len() == 2 {
        ExitCode::Success
    } else if args.len() < 2 {
        ExitCode::Failure(1)
    } else {
        ExitCode::Failure(2)
    };

    println!("{exit:?}");
    let code = match exit {
        ExitCode::Success => {
            println!("success");
            0
        }
        ExitCode::Failure(err) => {
            println!("Error: {err}");
            err
        }
    };
    println!("{:?}", code);
}
  • Apparently the standard library of Rust uses a struct to represent an ExitCode.

Enumeration of the 7 days of the week

  • We can then assign one of the days to a variable and then we can use a match to know which day it is.
enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}

fn main() {
    handle_day(Weekday::Sunday);

    let sat = Weekday::Saturday;
    handle_day(sat);

    let weekdays = [
        Weekday::Monday,
        Weekday::Tuesday,
        Weekday::Wednesday,
        Weekday::Thursday,
        Weekday::Friday,
    ];

    for day in weekdays {
        handle_day(day);
    }
}

fn handle_day(day: Weekday) {
    match day {
        Weekday::Monday => println!("Today is Monday"),
        Weekday::Tuesday => println!("Today is Tuesday"),
        Weekday::Wednesday => println!("Today is Wednesday"),
        Weekday::Thursday => println!("Today is Thursday"),
        Weekday::Friday => println!("Today is Friday"),
        Weekday::Saturday => println!("Today is Saturday"),
        Weekday::Sunday => println!("Today is Sunday"),
    }
}
Today is Sunday
Today is Saturday
Today is Monday
Today is Tuesday
Today is Wednesday
Today is Thursday
Today is Friday

Enumeration with non-exhaustive patterns

  • enum

  • dead_code

  • In this example in the match we don't hanle every variant of the enum and thus we have to handle the "deafult" case using and _ underscore.

  • Try running this code after commenting out the row handline _.

#[derive(Debug)]
#[allow(dead_code)]
enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}

// We need the allow dead_code beacause in this example
// we did not use all the values that were listed in the enum

fn main() {
    let today = Weekday::Sunday;

    println!("{:?}", today);
    println!();
    for day in [
        Weekday::Monday,
        Weekday::Tuesday,
        Weekday::Saturday,
        Weekday::Sunday,
    ] {
        println!("{:?}", day);
        match day {
            Weekday::Sunday => println!("Today is {day:?}, it is a day off in Europe"),
            Weekday::Saturday => println!("Today is {day:?}, it is a day off in Israel"),
            _ => println!("Today is {day:?}, it is a workday"),
        }
    }
}
Sunday

Monday
Today is Monday, it is a workday
Tuesday
Today is Tuesday, it is a workday
Saturday
Today is Saturday, it is a day off in Israel
Sunday
Today is Sunday, it is a day off in Europe

Enumeration and manual comparision

  • PartialEq

We can also compare variables holding enum variants, but for that to work we also need to derivede from the ParialEq trait. Basically we need to implement the operation that allows use to compare two values of this type.

#[derive(PartialEq)]
#[allow(dead_code)]
enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}

// We need the allow dead_code beacause in this example
// we did not use all the values that were listed in the enum

fn main() {
    let today = Weekday::Saturday;
    let tomorrow = Weekday::Sunday;
    let market = Weekday::Sunday;

    if market == today {
        println!("Today is market day");
    }
    if market == tomorrow {
        println!("Tomorrow is market day");
    }
}
Tomorrow is market day

Enumeration and order

  • PartialEq
  • PartialOrd
#[derive(PartialOrd, PartialEq)]
#[allow(dead_code)]
enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}

// We need the allow dead_code beacause in this example
// we did not use all the values that were listed in the enum

fn main() {
    let yesterday = Weekday::Friday;
    let today = Weekday::Saturday;
    let tomorrow = Weekday::Sunday;

    if yesterday < today {
        println!("Today is after yesterday");
    }
    if today < tomorrow {
        println!("Tomorrow is after today");
    }
}
Today is after yesterday
Tomorrow is after today

Enumeration colors - functions and String

  • enum

  • dead_code

  • PartialEq

  • Similar example with colors with functions to convert to RGB and from RGB values.

#[derive(Debug, PartialEq)]
#[allow(dead_code)]
enum Color {
    Red,
    Green,
    Blue,
    White,
    Black,
    Other(String),
}

fn to_rgb(color: &Color) -> String {
    match color {
        Color::Black => String::from("000000"),
        Color::Red => String::from("ff0000"),
        Color::Green => String::from("00ff00"),
        Color::Blue => String::from("0000ff"),
        Color::White => String::from("ffffff"),
        Color::Other(val) => val.clone(),
    }
}

fn from_rgb(rgb: &str) -> Color {
    match rgb.to_ascii_lowercase().as_str() {
        "000000" => Color::Black,
        "ff0000" => Color::Red,
        "00ff00" => Color::Green,
        "0000ff" => Color::Blue,
        "ffffff" => Color::White,
        val => Color::Other(val.to_owned()),
    }
}

fn main() {
    let background = Color::Black;
    let foreground = Color::White;
    let ink = Color::Black;
    let sky = Color::Blue;
    let other = Color::Other(String::from("4674b9"));

    let experiment = from_rgb("ab89e2");

    println!("{}", background == foreground);
    assert_ne!(background, foreground);
    println!("{}", background == ink);
    assert_eq!(background, ink);
    println!();

    for color in [&background, &foreground, &ink, &sky, &other, &experiment] {
        println!("{} {:?}", to_rgb(color), color);
    }

    assert_eq!(to_rgb(&background), "000000");
    assert_eq!(to_rgb(&other), "4674b9");
}
false
true

000000 Black
ffffff White
000000 Black
0000ff Blue
4674b9 Other("4674b9")
ab89e2 Other("ab89e2")

Enumeration colors - functions and str

  • This example is similar, but instead of storing owned string we are using str in the enum.
#[derive(Debug, PartialEq)]
#[allow(dead_code)]
enum Color<'a> {
    Red,
    Green,
    Blue,
    White,
    Black,
    Other(&'a str),
}

fn to_rgb<'a>(color: &'a Color) -> &'a str {
    match color {
        Color::Black => "000000",
        Color::Red => "ff0000",
        Color::Green => "00ff00",
        Color::Blue => "0000ff",
        Color::White => "ffffff",
        &Color::Other(val) => val,
    }
}

fn from_rgb(rgb: &str) -> Color {
    match rgb {
        "000000" => Color::Black,
        "ff0000" => Color::Red,
        "00ff00" => Color::Green,
        "0000ff" => Color::Blue,
        "ffffff" => Color::White,
        val => Color::Other(val),
    }
}

fn main() {
    let background = Color::Black;
    let foreground = Color::White;
    let ink = Color::Black;
    let sky = Color::Blue;
    let other = Color::Other("4674b9");

    let experiment = from_rgb("ab89e2");

    println!("{}", background == foreground);
    assert_ne!(background, foreground);
    println!("{}", background == ink);
    assert_eq!(background, ink);
    println!();

    for color in [&background, &foreground, &ink, &sky, &other, &experiment] {
        println!("{} {:?}", to_rgb(color), color);
    }

    assert_eq!(to_rgb(&background), "000000");
    assert_eq!(to_rgb(&sky), "0000ff");
    assert_eq!(to_rgb(&other), "4674b9");
}
false
true

000000 Black
ffffff White
000000 Black
0000ff Blue
4674b9 Other("4674b9")
ab89e2 Other("ab89e2")

Enumeration colors - with method

  • enum
  • Debug
  • impl
  • self
#[derive(Debug)]
#[allow(dead_code)]
enum Color {
    Red,
    Green,
    Blue,
    White,
    Black,
    Other(String),
}

impl Color {
    fn to_rgb(&self) -> &str {
        return match self {
            Color::Black => "000000",
            Color::Red => "ff0000",
            Color::Green => "00ff00",
            Color::Blue => "0000ff",
            Color::White => "ffffff",
            Color::Other(val) => val.as_str(),
        };
    }
}

fn main() {
    let background = Color::Black;
    let foreground = Color::White;
    let ink = Color::Black;
    let sky = Color::Blue;
    let other = Color::Other(String::from("4674b9"));

    for color in [background, foreground, ink, sky, other] {
        println!("{:?}  {}", color, color.to_rgb());
    }
}
Black  000000
White  ffffff
Black  000000
Blue  0000ff
Other("4674b9")  4674b9

Enumeration colors - with constructor

  • impl
  • Self
#[derive(Debug)]
#[allow(dead_code)]
enum Color<'a> {
    Red,
    Green,
    Blue,
    White,
    Black,
    Other(&'a str),
}

impl<'a> Color<'a> {
    fn from_rgb(rgb: &'a str) -> Self {
        return match rgb {
            "000000" => Color::Black,
            "ff0000" => Color::Red,
            "00ff00" => Color::Green,
            "0000ff" => Color::Blue,
            "ffffff" => Color::White,
            val => Color::Other(val),
            //_ => panic!("Unhandled color {rgb}"),
        };
    }
}

fn main() {
    let background = Color::from_rgb("000000");
    let foreground = Color::from_rgb("ffffff");
    let ink = Color::from_rgb("000000");
    let sky = Color::Blue;
    let other = Color::Other("4674b9");

    for color in [background, foreground, ink, sky, other] {
        println!("{:?}", color);
    }
}
Black
White
Black
Blue
Other("4674b9")

Enumerate without PartialEq using match

  • enum

  • match

  • dead_code

  • If we don't have PartialEq on an enum and we don't want to add it or cannot add it (e.g. because it was supplied by an external crate) we can use match:

#[allow(dead_code)]
enum ColorName {
    Red,
    Green,
    Blue,
    White,
    Black,
}

fn main() {
    let background = ColorName::Black;
    let foreground = ColorName::White;
    let ink = ColorName::Black;
    let frame = ColorName::Red;

    for color in [background, foreground, ink, frame] {
        match color {
            ColorName::White => println!("white: #FFFFFF"),
            ColorName::Red => println!("red: #FF0000"),
            _ => println!("other"),
        }
    }
}
other
white: #FFFFFF
other
red: #FF0000

Struct using enum

  • struct
  • enum
#[allow(dead_code)]
enum ColorName {
    Red,
    Green,
    Blue,
    White,
    Black,
}

struct Color {
    name: ColorName,
    rgb: String,
}

fn main() {
    let background = Color {
        name: ColorName::Black,
        rgb: String::from("#000000"),
    };
    let foreground = Color {
        name: ColorName::White,
        rgb: String::from("#FFFFFF"),
    };
    let ink = Color {
        name: ColorName::Black,
        rgb: String::from("#FFFFFF"),
    };
    let frame = Color {
        name: ColorName::Red,
        rgb: String::from("#FF0000"),
    };

    for color in [background, foreground, ink, frame] {
        match color.name {
            ColorName::White => println!("white: {}", color.rgb),
            ColorName::Red => println!("red: {}", color.rgb),
            _ => println!("other"),
        }
    }
}
other
white: #FFFFFF
other
red: #FF0000

Exercise: enum for programming languages

  • Create an enum representing "all" the programming languages. (py for Python, rs for Rust)
  • Add a constructor to return the enum based on the file extension.
  • Remember, Perl uses both pl and pm as extensions.
  • Write tests that will check some of the cases.

Solution: enum for programming languages

#![allow(dead_code)]

#[derive(Debug, PartialEq)]
enum Language {
    Perl,
    Python,
    Rust,
}

impl Language {
    fn from_ext(ext: &str) -> Self {
        match ext {
            "pl" => Language::Perl,
            "py" => Language::Python,
            _ => panic!("No such language"),
        }
    }

    fn option_from_ext(ext: &str) -> Option<Self> {
        match ext {
            "pl" => Some(Language::Perl),
            "py" => Some(Language::Python),
            _ => panic!("No such language"),
        }
    }
}
fn main() {
    println!("Hello, world!");
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_func() {
        let lang = Language::from_ext("pl");
        assert_eq!(lang, Language::Perl);
    }
}

// TODO how to handle when the user callse from_ext with an unknonw extension?
// Have a variant Unknown and set that?
// panic! ?
// Should the from_ext return an Option  and in this should it be None?
// Should the from_ext return a Result  and in this should it be Error?

The Result enum

The Result enum

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

TODO: show the constructor example without lifetimes TODO: Show ordering of simple enums and enums that can have different variants TODO: show enum where one of the variants have more than one values or a value which is complex (e.g. tuple or struct)

Iterate over the variants of an enum

Sometimes we need to go over all the variants of an enum and do something for each one of them.

[package]
name = "list-variants"
version = "0.1.0"
edition = "2021"

[dependencies]
strum = "0.26.3"
strum_macros = "0.26.4"
use strum::IntoEnumIterator;
use strum_macros::EnumIter;

#[derive(EnumIter, Debug, PartialEq)]
enum Animal {
    Cat,
    Dog,
    Snake,
    Camel,
    Crab,
}

fn main() {
    for animal in Animal::iter() {
        println!("{animal:?}");
    }
}
Cat
Dog
Snake
Camel
Crab

Files

Rust - write to file

use std::fs::File;
use std::io::Write;

fn main() {
    let filename = "data.txt";
    let mut file = File::create(filename).unwrap();
    writeln!(&mut file, "Hello World!").unwrap();
}

Rust - read content of a file as a string

  • open
  • read_to_string
use std::env;
use std::fs::File;
use std::io::Read;

fn main() {
    let filename = get_filename();
    match File::open(&filename) {
        Ok(mut file) => {
            let mut content = String::new();

            match file.read_to_string(&mut content) {
                Ok(size) => {
                    println!("Read {size} bytes.");
                    println!("We have a string of {} bytes.", content.len());
                    println!("{content}");
                }
                Err(err) => eprintln!("Error: {err}"),
            }
        }
        Err(error) => {
            eprintln!("Error opening file {filename}: {error}");
        }
    }
}

fn get_filename() -> String {
    let args = env::args().collect::<Vec<_>>();
    if args.len() != 2 {
        eprintln!("Usage: {} data.txt", args[0])
    }

    args[1].to_owned()
}

Rust - read file line-by-line

  • BufRead
  • BufReader
  • lines
use std::fs::File;
use std::io::{BufRead, BufReader};

fn main() {
    let filename = "rust.json";
    match File::open(filename) {
        Ok(file) => {
            let reader = BufReader::new(file);
            for line in reader.lines() {
                let line = line.unwrap();
                println!("{}", line);
            }
        }
        Err(error) => {
            println!("Error opening file {}: {}", filename, error);
        }
    }
}

Rust - read file line-by-line with row number (enumerate)

  • enumerate
  • lines
use std::fs::File;
use std::io::{BufRead, BufReader};

fn main() {
    let filename = "rust.json";
    match File::open(filename) {
        Ok(file) => {
            let reader = BufReader::new(file);
            for (index, line) in reader.lines().enumerate() {
                let line = line.unwrap();
                println!("{}. {}", index + 1, line);
            }
        }
        Err(error) => {
            println!("Error opening file {}: {}", filename, error);
        }
    }
}

Rust - counter

  • Read
  • Write
  • File
  • read_to_string
  • create
use std::fs::File;
use std::io::Read;
use std::io::Write;

fn main() {
    let filename = "count.txt";
    let mut content = String::new();

    match File::open(filename) {
        Ok(mut file) => {
            file.read_to_string(&mut content).unwrap();
        }
        Err(_) => {
            content = String::from("0");
        }
    }
    let mut counter: i32 = content.trim().parse().expect("Wanted a number");
    counter += 1;
    println!("{}", counter);

    let mut file = File::create(filename).unwrap();
    writeln!(&mut file, "{}", counter).unwrap();
}

Rust list content of directory

use std::env;
use std::path::Path;
use std::process::exit;

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() > 2 {
        println!("More than enough command line parameters");
        exit(1);
    }

    let mut path = Path::new(".");
    if args.len() == 2 {
        println!("{}", args[1]);
        path = Path::new(&args[1]);
    }

    for entry in path.read_dir().expect("read_dir call failed").flatten() {
        println!("{:?}", entry.path());
    }
}

Rust list directory content recursively (tree)

use std::env;
use std::fs;
use std::path::Path;
use std::process::exit;

fn main() {
    let path = get_path();

    list_dir(Path::new(&path));
}

fn get_path() -> String {
    let args: Vec<String> = env::args().collect();

    if args.len() > 2 {
        println!("More than enough command line parameters");
        exit(1);
    }

    if args.len() == 2 {
        println!("{}", args[1]);
        return args[1].to_owned();
    }

    String::from(".")
}

fn list_dir(path: &Path) {
    for entry in path.read_dir().expect("read_dir call failed").flatten() {
        println!("{:?}", entry.path());
        let metadata = fs::metadata(entry.path());
        let file_type = metadata.expect("Could not access file").file_type();
        if file_type.is_dir() {
            list_dir(&entry.path());
        }
    }
}
use std::env;
use std::fs;
use std::path::Path;
use std::process::exit;

fn main() {
    let path = get_path();

    let tree = list_dir(Path::new(&path));
    for path in tree {
        println!("{path}");
    }
}

fn get_path() -> String {
    let args: Vec<String> = env::args().collect();

    if args.len() > 2 {
        println!("More than enough command line parameters");
        exit(1);
    }

    if args.len() == 2 {
        println!("{}", args[1]);
        return args[1].to_owned();
    }

    String::from(".")
}

fn list_dir(path: &Path) -> Vec<String> {
    let mut pathes: Vec<String> = vec![];
    for entry in path.read_dir().expect("read_dir call failed").flatten() {
        //println!("{:?}", entry.path());
        pathes.push(entry.path().to_str().unwrap().to_owned());
        let metadata = fs::metadata(entry.path());
        let file_type = metadata.expect("Could not access file").file_type();
        if file_type.is_dir() {
            pathes.extend(list_dir(&entry.path()));
        }
    }

    pathes
}

Makedir

use std::fs;

fn main() {
    match makedir() {
        Ok(res) => {
            dbg!(res);
        }
        Err(err) => {
            dbg!("error: {}", err);
        }
    }
}

fn makedir() -> std::io::Result<()> {
    //fs::create_dir("paren/sub")?;
    fs::create_dir("folder")?;
    Ok(())
}

Makedirs

use std::fs;

fn main() {
    match makedirs() {
        Ok(res) => {
            dbg!(res);
        }
        Err(_) => {
            dbg!("error");
        }
    }
}

fn makedirs() -> std::io::Result<()> {
    fs::create_dir_all("parent/sub")?;
    Ok(())
}

Get the temporary directory

  • temp_dir
use std::env;

fn main() {
    let folder = env::temp_dir();
    println!("{:?}", folder);
}

Create temporary directory

  • tempdir
[package]
name = "tempdir-demo"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tempdir = "0.3"
use std::fs::File;
use std::io::{self, Write};
use tempdir::TempDir;

fn main() {
    match write_temp_folder_with_files() {
        Ok(()) => println!("ok"),
        Err(err) => println!("Error {err}"),
    }
}

fn write_temp_folder_with_files() -> io::Result<()> {
    let temp_dir = TempDir::new("demo")?;
    let file_path = temp_dir.path().join("foo.txt");
    println!("{:?}", file_path);

    let mut fh = File::create(file_path)?;
    fh.write_all(b"Hello, world!")?;
    fh.sync_all()?;
    temp_dir.close()?;

    Ok(())
}

Current working directory

use std::env::current_dir;

fn main() {
    let folder = current_dir().unwrap();
    println!("{:?}", folder);
    println!("{}", folder.display());
}

Change directory

  • set_current_dir
  • chdir
  • cd
use std::env;

fn main() {
    let folder = env::current_dir().unwrap();
    println!("{:?}", folder);

    env::set_current_dir("/tmp").unwrap();

    let folder = env::current_dir().unwrap();
    println!("{:?}", folder);
}

Append to file

  • append
  • open
use std::fs::File;
use std::io::Write;

fn main() {
    let filename = "data.txt";
    let mut fh = match File::options().append(true).open(filename) {
        Ok(fh) => fh,
        Err(_) => File::create(filename).unwrap(),
    };
    writeln!(&mut fh, "Hello").unwrap();
}

Show size of file

  • size
  • len
fn main() {
    let path = "du.rs";
    let meta = match std::fs::metadata(path) {
        Ok(val) => val,
        Err(_) => panic!("error"),
    };
    dbg!(meta.len());
}

du - disk usage

  • metadata
  • len
  • read_dir
use std::path::Path;
use std::process::exit;

// get path on the command line
// traverse the directory tree

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let path = if args.len() == 1 {
        Path::new(".")
    } else {
        Path::new(&args[1])
    };
    if !path.exists() {
        eprintln!("given path '{:?}' does not exist", path);
        exit(1);
    }

    let total = get_total(path);
    println!("Total: {}", total);
}

fn get_total(path: &Path) -> u64 {
    //dbg!(path);

    let mut total: u64 = 0;
    if path.is_dir() {
        for entry in path.read_dir().expect("read_dir call failed").flatten() {
            total += get_total(&entry.path());
        }
        return total;
    }

    match std::fs::metadata(path) {
        Ok(meta) => {
            println!("{:?} {:?}", meta.len(), path);
            meta.len()
        }
        Err(_) => {
            eprintln!("Error in {:?}", path);
            0
        }
    }
}

Error handling in file operations

  • unwrap
  • expect
  • match
  • Ok
  • Err
  • panic!
fn main() {
    let args = std::env::args().collect::<Vec<String>>();
    if args.len() != 2 {
        println!("Usage: {} expect", args[0]);
        std::process::exit(1);
    }
    let command = &args[1];

    let text = "Hello, world!";
    let filename = "other/hello.txt";
    match command.as_str() {
        "nothing" => {
            let _ = std::fs::write(filename, text);
            panic!("This will panic");
        },
        "unwrap" => std::fs::write(filename, text).unwrap(),
        "expect" => std::fs::write(filename, text).expect(format!("Should write text to file {filename:?}").as_str()),
        "match" => match std::fs::write(filename, text) {
            Ok(_) => println!("Write text to file successfully"),
            Err(err) => {
                println!("Failed to write text to file {filename:?}: '{err}'");
                match err.kind() {
                    std::io::ErrorKind::PermissionDenied => println!("Permission denied"),
                    std::io::ErrorKind::NotFound => println!("File not found"),
                    // There are many other error types, but we are not handling them here
                    _ => println!("Other error"),
                }
            }
        },
        other => println!("Unknown command: {}", other),
    }
}

Exercise count digits in file

Write a function that receives a string as a parameter and returns an array of 10 numbers, the number of each digit in the file. At first you can assume the file contains a single row containing only digits: 234566. Then make the input file more complex

  • Having multiple rows and including digits and spaces.
  • Having multiple rows and including any character.

In each case we only need to count the number of each digit.

Exercise - wc (word count)

Implement the default behaviour of the wc command of Linux/Unix. For each file showing

  • number of lines
  • number of words
  • number of bytes
$ wc intro.md files.md strings.md
  182   519  5658 intro.md
  162   273  3133 files.md
  345   943  9708 strings.md
  689  1735 18499 total

Exercise - simple grep

  • Implement a simple version of grep that receives a search-term and a file and shows the lines in the file that match.

  • Extend it to be able to work on more than one file.

  • Extend it to accept regex as the search-term

  • Extend it to accept an -r or --recursive flag and if given a folder then process each file recursively.

Exercise - du (disk usage)

  • Implement the Linux/Unix du command:

  • Given a filename shows the size of the file.

  • Given a folder shows the file sizes in the whole directory tree.

  • Given the -s flag shows a summary instead of all the files.

Solution: count digits in file

  • chars
  • usize
  • enumerate
fn main() {
    let text = get_text();
    let counter = count_digits(&text);
    display(&counter);
}

fn count_digits(text: &str) -> [i8; 10] {
    let mut counter: [i8; 10] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
    for ch in text.chars() {
        if ch == ' ' {
            continue;
        }
        if !"0123456789".contains(ch) {
            continue;
        }

        let ix = ch as usize - '0' as usize;
        //println!("{ch}");
        //println!("{ix}");
        counter[ix] += 1;
    }

    counter
}

fn display(counter: &[i8]) {
    for (ix, count) in counter.iter().enumerate() {
        println!("{ix}: {}", count);
    }
}

fn get_text() -> String {
    let args = std::env::args().collect::<Vec<_>>();
    if args.len() != 2 {
        eprintln!("Usage: {} FILENAME", args[0]);
        std::process::exit(1)
    }

    std::fs::read_to_string(&args[1]).unwrap()
}
0: 0
1: 2
2: 1
3: 1
4: 1
5: 1
6: 1
7: 1
8: 2
9: 0

Path

Return vector of Path or PathBuf

  • Path

  • PathBuf

  • We would like to create a function that collectes Path objects and returns them.

  • Instead of Path objects we'll return PathBuf objects

use std::path::{Path, PathBuf};

fn main() {
    let pathes = get_pathes();
    println!("{:?}", pathes);
    for path in pathes {
        println!("{:?}", path);
        //println!("{}", path.display());
        //println!("{:?}", path.parent());
    }
}

fn get_pathes() -> Vec<PathBuf> {
    let mut entries = vec![];
    let path = Path::new(".");
    for entry in path.read_dir().expect("read_dir call failed").flatten() {
        entries.push(entry.path());
    }
    entries
}

Convert the PathBuf to String to compare

  • current_dir
  • clone
  • into_os_string
  • into_string
  • unwrap
fn main() {
    let cwd = std::env::current_dir().unwrap();
    println!("{:?}", cwd);
    //if cwd == "/home/gabor/work/slides/rust/examples/path/convert-pathbuf-to-string" {
    //    ^^ no implementation for `PathBuf == &str`

    if cwd.clone().into_os_string().into_string().unwrap()
        == "/home/gabor/work/slides/rust/examples/path/convert-pathbuf-to-string"
    {
        println!("at home");
    } else {
        println!("somewhere else");
    }
}

get file extension

  • Path
  • extension
use std::path::Path;

fn main() {
    let path = Path::new("a/b/code.rs");
    println!("{:?}", path.extension().unwrap());
    println!("{:?}", path.extension().unwrap() == "rs");

    let path = Path::new("a/b/code");
    match path.extension() {
        Some(value) => println!("{:?}", value),
        None => println!("Has no extension"),
    };
}

file extension

fn main() {
    for path in [
        "hello.rs",
        ".hello.swp",
        "hello.and.swp",
        ".github",
        ".bashrc",
        "dotless",
    ] {
        let path = std::path::Path::new(path);
        match path.extension() {
            Some(extension) => println!("{:15} {:?}", path.display(), extension),
            None => println!("{:15} None", path.display()),
        }
    }
}
hello.rs        "rs"
.hello.swp      "swp"
hello.and.swp   "swp"
.github         None
.bashrc         None
dotless         None

parent directory

use std::path::Path;

fn main() {
    let path = Path::new("one/two/three/four.rs");
    println!("{}", path.display());

    println!("{}", path.parent().unwrap().display());

    match Path::new("/a/b").parent() {
        Some(val) => println!("ok {}", val.display()),
        None => println!("No parent"),
    }

    match Path::new("/a").parent() {
        Some(val) => println!("ok {}", val.display()),
        None => println!("No parent"),
    }

    match Path::new("/").parent() {
        Some(val) => println!("ok {}", val.display()),
        None => println!("No parent"),
    }

    match Path::new("").parent() {
        Some(val) => println!("ok {}", val.display()),
        None => println!("No parent"),
    }
}
one/two/three/four.rs
one/two/three
ok /a
ok /
No parent
No parent

directory ancestors (parent directories)

use std::path::Path;

fn main() {
    let path = Path::new("one/two/three/four.rs");
    println!("{}", path.display());

    for p in path.ancestors() {
        println!("{}", p.display());
    }
}
one/two/three/four.rs
one/two/three/four.rs
one/two/three
one/two
one

directory ancestor (n level)

  • ancestor
  • ancestors
  • nth
  • next
use std::path::Path;

#[allow(clippy::iter_nth_zero)]
fn main() {
    let path = Path::new("one/two/three/four/five/six.rs");

    //match ancestor(&path, 0) {
    //    Some(val) => println!("ancestor: '{}'", val.display()),
    //    None => println!("No such ancestor"),
    //}
    assert_eq!(
        ancestor(path, 0),
        Some(Path::new("one/two/three/four/five/six.rs"))
    );
    assert_eq!(
        ancestor(path, 1),
        Some(Path::new("one/two/three/four/five"))
    );
    assert_eq!(ancestor(path, 5), Some(Path::new("one")));
    assert_eq!(ancestor(path, 6), Some(Path::new("")));
    assert_eq!(ancestor(path, 7), None);

    assert_eq!(
        parentn(path, 0),
        Some(Path::new("one/two/three/four/five/six.rs"))
    );
    assert_eq!(parentn(path, 1), Some(Path::new("one/two/three/four/five")));
    assert_eq!(parentn(path, 5), Some(Path::new("one")));
    assert_eq!(parentn(path, 6), Some(Path::new("")));
    assert_eq!(parentn(path, 7), None);

    assert_eq!(
        path.ancestors().next(),
        Some(Path::new("one/two/three/four/five/six.rs"))
    );
    assert_eq!(
        path.ancestors().nth(0),
        Some(Path::new("one/two/three/four/five/six.rs"))
    );
    assert_eq!(
        path.ancestors().nth(1),
        Some(Path::new("one/two/three/four/five"))
    );
    assert_eq!(path.ancestors().nth(5), Some(Path::new("one")));
    assert_eq!(path.ancestors().nth(6), Some(Path::new("")));
    assert_eq!(path.ancestors().nth(7), None);
}

//fn ancestor(mut path: &Path, mut n: u16) -> Option<&Path> {
//    while n > 0 {
//        match path.parent() {
//            Some(value) => path = value,
//            None => return None,
//        }
//        n = n - 1;
//    }
//    return Some(path);
//}

// improved by Alice Ryhl
fn ancestor(mut path: &Path, n: u16) -> Option<&Path> {
    for _ in 0..n {
        path = path.parent()?
    }
    Some(path)
}

// recursive
fn parentn(path: &Path, n: u16) -> Option<&Path> {
    if n == 0 {
        return Some(path);
    }
    match path.parent() {
        Some(value) => parentn(value, n - 1),
        None => None,
    }
}
one/two/three/four/five/six.rs
ancestor: 'one/two/three/four/five/six.rs'
ancestor: 'one/two/three/four/five'
ancestor: 'one'
ancestor: ''
No such ancestor

join pathes

  • join
use std::path::Path;

fn main() {
    let path = Path::new("/home/foo");
    println!("{}", path.display());

    let child = path.join("work");
    println!("{}", child.display());

    let grand_child = child.join("slides");
    println!("{}", grand_child.display());

    let rel_path = Path::new("osdc/project");
    let other = path.join(rel_path);
    println!("{}", other.display());
}
/home/foo
/home/foo/work
/home/foo/work/slides
/home/foo/osdc/project

basename (file_name)

  • file_name
  • basename
use std::path::Path;

fn main() {
    let path = Path::new("root/subdir/folder/filename.rs");
    println!("{}", path.file_name().unwrap().to_str().unwrap());
}

Relative and absolute path

  • canonicalize
fn main() {
    let args = std::env::args().collect::<Vec<String>>();
    if args.len() != 2 {
        eprintln!("Usage: {} PATH", &args[0]);
        std::process::exit(1);
    }

    let path = std::path::PathBuf::from(&args[1]);

    println!("relative: {}", path.as_os_str().to_str().unwrap());
    println!(
        "absolute: {}",
        path.canonicalize().unwrap().as_os_str().to_str().unwrap()
    );
}

List content of a directory - listdir

  • read_dir
  • file_name
  • canonicalize
fn main() {
    let args = std::env::args().collect::<Vec<String>>();
    if args.len() != 2 {
        eprintln!("Usage: {} PATH", &args[0]);
        std::process::exit(1);
    }
    let path = std::path::PathBuf::from(&args[1]);

    let filenames = path
        .read_dir()
        .unwrap()
        .map(|de| de.unwrap().file_name().to_str().unwrap().to_owned())
        .collect::<Vec<String>>();

    println!("{:?}", filenames);
    println!();

    let relative_pathes = path
        .read_dir()
        .unwrap()
        .map(|de| de.unwrap().path().as_os_str().to_str().unwrap().to_owned())
        .collect::<Vec<String>>();

    println!("{:?}", relative_pathes);
    println!();

    let absolute_pathes = path
        .read_dir()
        .unwrap()
        .map(|de| {
            de.unwrap()
                .path()
                .canonicalize()
                .unwrap()
                .as_os_str()
                .to_str()
                .unwrap()
                .to_owned()
        })
        .collect::<Vec<String>>();

    println!("{:?}", absolute_pathes);
}
["main.rs"]

["src/main.rs"]

["/home/gabor/work/rust.code-maven.com/slides/rust/examples/path/list-dir/src/main.rs"]

List dir recursively

use std::path::Path;

fn main() {
    let path = get_dirname();
    let path = Path::new(&path);
    list_dir(path);
}

fn get_dirname() -> String {
    let args = std::env::args().collect::<Vec<_>>();
    if args.len() != 2 {
        eprintln!("Usage: {} PATH", args[0]);
        std::process::exit(1);
    }

    args[1].to_owned()
}

fn list_dir(path: &Path) {
    println!("path: {:?}", path);
    match path.read_dir() {
        Ok(dh) => {
            // A ReadDir instance
            for entry in dh {
                println!("{:?}", entry);
                if let Ok(entry) = entry {
                    if entry.path().is_dir() {
                        println!("DIR");
                        list_dir(&entry.path());
                    }
                }
            }
        }
        Err(err) => println!("Could not open directory '{:?}': {}", path, err),
    }
}

Functions

Rust main function

  • fn

  • main

  • There are two types of crates: libraries and executables. The latter can also contain libraries.

  • For executable crates we must have a file called src/main.rs and it must have a function called main.

  • This is the most basic definition of a main function. It does nothing.

fn main() {}

Rust hello world function

  • fn

  • We can declare functions with other names using the fn keyword.

  • The relative location of the functions does not matter. There is no need to defined headers.

  • We can call the functions by writing their name followed by a pair of parenthesis.

fn main() {
    greet();
}

fn greet() {
    println!("Hello World!");
}

Rust function with parameter (&str)

  • &str

  • Functions can expect parameters.

  • We have to define the type of the parameter.

  • In this case we are expecting a string slice &str.

  • If we already have a string slice we can pass it as it is.

  • If we have a String then we have to prefix it with &.

fn main() {
    greet("Foo");
    greet("Bar");
    println!();

    let name = "Foo";
    greet(name);
    greet(name);
    println!();

    let name = String::from("Bar");
    greet(&name);
    greet(&name);
}

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

Hello Foo!
Hello Foo!

Hello Bar!
Hello Bar!

Rust functions return the unit by default

  • By default function return nothing, more precisely they return () the empty tuple which is called the unit.
  • We need the #[allow(clippy::let_unit_value, clippy::unit_cmp)] in the example to make Clippy, the Rust linter accept this code.
#[allow(clippy::let_unit_value, clippy::unit_cmp)]

fn main() {
    let res = hello_world();
    assert_eq!(res, ());
    println!("{:?}", res);
}

fn hello_world() {
    println!("Hello, world!");
}
Hello, world!
()

Rust function return value (integer i32)

  • return

  • i32

  • After an arrow -> we can add the type of the return value.

  • We can then return that value by using the return statement.

  • #[allow(clippy::needless_return)] is there to silence clippy, the linter.

fn main() {
    println!("{}", add(2, 3));
    println!("{}", add(3, 4));
}

fn add(x: i32, y: i32) -> i32 {
    #[allow(clippy::needless_return)]
    return x + y;
}

Return the last expression (no return)

  • If the last thing in the function is an expression (no semi-colon at the end) then this is the returned value.
  • No need for the return statement.
fn main() {
    println!("{}", add(2, 3));
    println!("{}", add(3, 4));
}

fn add(a: i32, b: i32) -> i32 {
    a + b // no semi-colon here!
}
5
7

Return a string

  • String
fn main() {
    let name = get_name();
    println!("{}", name);
}

fn get_name() -> String {
    let fname = "Foo";
    let lname = "Bar";
    let name = format!("{} {}", fname, lname);
    println!("{}", name);
    name
}

Rust recursive functions: factorial

fn main() {
    let n = 10;
    let fact = recursive_factorial(n);
    println!("Factorial of {n} is {fact}");
    let fact = factorial(n);
    println!("Factorial of {n} is {fact}");
}

fn recursive_factorial(n: i64) -> i64 {
    if n == 0 {
        return 1;
    }
    n * recursive_factorial(n - 1)
}

fn factorial(n: i64) -> i64 {
    let mut fact = 1;
    for i in 1..=n {
        fact *= i;
    }
    fact
}
Factorial of 10 is 3628800
Factorial of 10 is 3628800

Rust recursive functions: Fibonacci

fn main() {
    let n = 15;
    let fibo = recursive_fibonacci(n);
    println!("The {n}th Fibonacci number is {fibo}");
    let fibo = fibonacci(n);
    println!("The {n}th Fibonacci number is {fibo}");
}

fn recursive_fibonacci(n: i32) -> i32 {
    if n == 1 || n == 2 {
        return 1;
    }
    recursive_fibonacci(n - 1) + recursive_fibonacci(n - 2)
}

fn fibonacci(n: i32) -> i32 {
    let mut fib = vec![1, 1];
    if n == 1 || n == 2 {
        return 1;
    }
    for _ in 3..=n {
        fib.push(fib[fib.len() - 1] + fib[fib.len() - 2]);
    }
    fib[fib.len() - 1]
}

Make function argument mutable inside the function

  • mut

  • Sometimes you pass an argument and you would like to change that value inside the function (without chaning the external variable).

fn main() {
    let n = 1;
    println!("before: {n}");

    do_something(n);
    println!("after:  {n}");
}

fn do_something(mut val: i32) {
    println!("start:  {val}");
    val += 1;
    println!("end:    {val}");
}
before: 1
start:  1
end:    2
after:  1

Cannot decalre the same function twice

fn main() {
    add(3, 4);
    add("x", "y");
}

fn add(x: i32, y: i32) {
    let z = x + y;
    println!("{z}");
}

fn add(x: &str, y: &str) {
    println!("{x}{y}");
}
   Compiling declare-twice v0.1.0 (/home/gabor/work/slides/rust/examples/functions/declare-twice)
error[E0428]: the name `add` is defined multiple times
  --> src/main.rs:11:1
   |
6  | fn add(x: i32, y: i32) {
   | ---------------------- previous definition of the value `add` here
...
11 | fn add(x: &str, y: &str) {
   | ^^^^^^^^^^^^^^^^^^^^^^^^ `add` redefined here
   |
   = note: `add` must be defined only once in the value namespace of this module

error[E0308]: arguments to this function are incorrect
 --> src/main.rs:3:5
  |
3 |     add("x", "y");
  |     ^^^ ---  --- expected `i32`, found `&str`
  |         |
  |         expected `i32`, found `&str`
  |
note: function defined here
 --> src/main.rs:6:4
  |
6 | fn add(x: i32, y: i32) {
  |    ^^^ ------  ------

Some errors have detailed explanations: E0308, E0428.
For more information about an error, try `rustc --explain E0308`.
error: could not compile `declare-twice` (bin "declare-twice") due to 2 previous errors

Pass by reference to change external variable - Increment in a function

fn main() {
    let mut n = 1;
    println!("before: {n}");
    increment(&mut n);
    println!("after:  {n}");
}

fn increment(val: &mut i32) {
    println!("start:  {val}");
    *val += 1;
    println!("end:    {val}");
}
before: 1
start:  1
end:    2
after:  2

Count digits using functions

  • chars
  • iter
  • enumerate
fn main() {
    let text = "1213 456 788";
    let counter = count_digits(text);
    display(&counter);
}

fn count_digits(text: &str) -> [i8; 10] {
    let mut counter: [i8; 10] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
    for ch in text.chars() {
        if ch == ' ' {
            continue;
        }
        let ix = ch as usize - '0' as usize;
        //println!("{ch}");
        //println!("{ix}");
        counter[ix] += 1;
    }

    counter
}

fn display(counter: &[i8]) {
    for (ix, item) in counter.iter().enumerate() {
        println!("{ix}: {item}");
    }
}
0: 0
1: 2
2: 1
3: 1
4: 1
5: 1
6: 1
7: 1
8: 2
9: 0

Function returning multiple values

fn stats(a: f32, b: f32) -> (f32, f32) {
    (a + b, a * b)
}
fn main() {
    let (sum, multiple) = stats(4.0, 7.0);
    println!("{sum}, {multiple}");
}
11, 28

Function accepting multiple types (e.g. any type of numbers)

//fn add(a: f32, b: f32) -> f32 {
//    a + b
//}
//fn add(a: i32, b: i32) -> i32 {
//    a + b
//}

fn add<T: Into<f64>>(a: T, b: T) -> f64 {
    a.into() + b.into()
}

fn main() {
    let sum = add(4.0, 7.0);
    println!("{sum}");

    let sum = add(5, 1);
    println!("{sum}");
}
11
6

Function that can accept any number (any integer or any float)

[package]
name = "any_number"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
fn main() {
    let x = 12345i16;
    let y = 2345678i32;
    //println!("{}", x as i64 + y as i64);
    let z = any_number(x);
    println!("{}", z);
    println!("{}", any_number(y));
    println!("{}", any_number(5i8));
    //println!("{}", any_number(5i64));
    println!("{}", any_number(5.1));
}

//fn any_number<Integer: Into<i64> +  Copy + std::fmt::Debug + std::fmt::Display>(num: Integer) -> i64 {
//    //println!("{:?}", num);
//    num.into()
//}

//fn any_number<Integer: Into<i32> +  Copy + std::fmt::Debug + std::fmt::Display>(num: Integer) -> i32 {
//    //println!("{:?}", num);
//    num.into()
//}

fn any_number<Integer: Into<f64> + Copy + std::fmt::Debug + std::fmt::Display>(
    num: Integer,
) -> f64 {
    //println!("{:?}", num);
    num.into()
}

Exercise rectangle functions

In an earlier chapter we had an exercise to ask the use for the length and the width of a rectangle and print the area and the circumference to the screen.

Simplify it by moving the code that reads the number from STDIN into a function that we call twice. Once for the width and one for the length.

use std::io;
use std::io::Write;

fn main() {
    let length = get_number("Length");
    let width = get_number("Width");

    println!(
        "Length: {} Width: {}, Area: {} Circumference: {}",
        length,
        width,
        length * width,
        2.0 * (length + width)
    );
}

fn get_number(name: &str) -> f32 {
    let mut number = String::new();
    print!("{}: ", name);
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut number)
        .expect("Failed to get input");

    let number: f32 = number.trim().parse().expect("Could not convert to i32");
    number
}

Exercise: is prime?

Implement a function that receives a number and returns true if it is a prime number.

Implement another function that, given a number, will return different enum variants if the number was prime number, composite number, zero or one.

Scoped functions

  • We can defined a function inside a function and then it will be only available in that scope.
  • Surprisingly (or not) we can call the function anywhere inside the scope, even before its declaration.
fn main() {
    say_hi("before");

    fn say_hi(text: &str) {
        println!("hi {text}");
    }

    say_hi("after");

    other();
}

fn other() {
    println!("in other");

    // cannot find function `say_hi` in this scope
    //say_hi("other");
}
hi before
hi after
in other

Variable ownership in Rust

Stack and Heap

  • Stack (fixed size, push/pop)
  • Heap (any size, allocate/free)

Integers are copies

  • Make the copy mutable
fn main() {
    let a = 23;
    println!("{a}");

    let mut b = a;
    println!("{b}");

    b = 42;
    println!("{a}");
    println!("{b}");
}
23
23
23
42

Passing integers to functions and returning integer

fn add_integers(x: i32, y: i32) -> i32 {
    let z = x + y;
    #[allow(clippy::needless_return)]
    return z;
}

fn main() {
    let a = 23;
    println!("{a}");

    let b = 19;
    println!("{b}");
    println!();

    let c = add_integers(a, b);
    println!("{a}");
    println!("{b}");
    println!("{c}");
}
23
19

23
19
42

Mutable integers are copies

  • Both are mutable
fn main() {
    let mut a = 23;
    println!("{a}");

    let mut b = a;
    println!("{b}");

    a += 1;
    println!("{a}");
    println!("{b}");

    b += 1;
    println!("{a}");
    println!("{b}");
}
23
23
24
23
24
24

Immutable integers are copies

  • Only the original is mutable, the copy is not
fn main() {
    let mut a = 23;
    println!("{a}");

    let b = a;
    println!("{b}");

    a += 1;
    println!("{a}");
    println!("{b}");
}
23
23
24
23

Pass integer to function return changed value

fn increment(x: i32) -> i32 {
    x + 1
}

fn main() {
    let mut a = 1;
    println!("{a}");
    a = increment(a);
    println!("{a}");
}
1
2

Pass mutable reference of integer to function

fn increment(x: &mut i32) {
    *x += 1;
}

fn main() {
    let mut a = 1;
    println!("{a}");
    increment(&mut a);
    println!("{a}");
}
1
2

Literal string

  • The variable "name" points to a literal string. (aka. hard-coded string). The string can never change.
  • Because the variable is not mutable, we cannot even replace the content. (See error message generated by the commented out code.)
fn main() {
    let name = "Foo";
    let other = name;
    println!("{name}");
    println!("{other}");

    //name = "Foo Bar";

    println!("{name}");
    println!("{other}");
}
Foo
Foo
Foo
Foo
error[E0384]: cannot assign twice to immutable variable `name`
 --> examples/ownership/literal_string.rs:7:5
  |
2 |     let name = "Foo";
  |         ----
  |         |
  |         first assignment to `name`
  |         help: consider making this binding mutable: `mut name`
...
7 |     name = "Foo Bar";
  |     ^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0384`.

Literal string in mutable variable

  • mut

  • The variable can be made mutable using mut. Then it can be replaced, but the literal (hard-coded) string is baked into the code of the program and thus it cannot be changed runtime.

  • This is an str type.

fn main() {
    let mut name = "Foo";
    let other = name;
    println!("{name}");
    println!("{other}");

    name = "Foo Bar";

    //name.push_str(" Bar");

    println!("{name}");
    println!("{other}");
}
Foo
Foo
Foo Bar
Foo
error[E0599]: no method named `push_str` found for reference `&str` in the current scope
 --> examples/ownership/literal_string_in_mutable_variable.rs:9:10
  |
9 |     name.push_str(" Bar");
  |          ^^^^^^^^ method not found in `&str`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0599`.

Passing literal string to function

fn main() {
    let name = "Foo Bar";
    greet(name);
    greet(name);
}

fn greet(text: &str) {
    println!("Greet: {text}");
}
Greet: Foo Bar
Greet: Foo Bar

Mutable string in immutable variable

  • push_str

  • If we initialize the variable using String::from then the literal value is copied to the heap and it can be changed.

  • But if the variable is not mutable, then what's the point?

fn main() {
    let name = String::from("Foo");
    println!("{name}");

    name.push_str(" Bar");
    println!("{name}");
}
Foo
Bar
error[E0596]: cannot borrow `name` as mutable, as it is not declared as mutable
 --> examples/ownership/mutable_string_in_immutable_variable.rs:5:5
  |
5 |     name.push_str(" Bar");
  |     ^^^^^^^^^^^^^^^^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
2 |     let mut name = String::from("Foo");
  |         +++

error: aborting due to previous error

For more information about this error, try `rustc --explain E0596`.

Mutable string

  • push_str

  • If we initialize the varibale using String::from and make it mutable then we can change the string.

fn main() {
    let mut name = String::from("Foo");
    println!("{name}");

    name.push_str(" Bar");
    println!("{name}");
}
Foo
Foo Bar

Move strings

  • We can assign a variable that contains a "mutable string" to another variable

  • But this is called a "move" as we move the ownership of the data to the new variable.

  • After that we cannot use the old variable any more.

  • This will be fun when would like to change one of the variables or if we pass it to a function.

  • The variable "owns" the data.

  • If we assign the variable to another variable, we pass (move) the ownership.

fn main() {
    let name = String::from("Foo");
    println!("{name}");

    let other = name;
    println!("{other}");

    // println!("{name}"); // value borrowed here after move
}
Foo
Foo
error[E0382]: borrow of moved value: `name`
 --> examples/ownership/move_string.rs:8:15
  |
2 |     let name = String::from("Foo");
  |         ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
...
5 |     let other = name;
  |                 ---- value moved here
...
8 |     println!("{name}"); // value borrowed here after move
  |               ^^^^^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
5 |     let other = name.clone();
  |                     ++++++++

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.

Move mutable string

fn main() {
    let name = String::from("Foo");
    println!("{name}");

    let other = name;
    println!("{other}");

    // println!("{name}"); // value borrowed here after move
}

Rust clone a String

  • clone

  • In some cases what we will want is to copy the content of the variable.

  • For this we can use the clone method.

macro_rules! prt {
    ($var:expr) => {
        println!(
            "{:2} {:2} {:p} {:15?} '{}'",
            $var.len(),
            $var.capacity(),
            &$var,
            $var.as_ptr(),
            $var
        );
    };
}

fn main() {
    let x = String::from("Foo Bar");
    prt!(x);
    let y = x.clone();
    prt!(x);
    prt!(y);
}
 7  7 0x7ffdaf695048  0x648f6a2b5b80 'Foo Bar'
 7  7 0x7ffdaf695048  0x648f6a2b5b80 'Foo Bar'
 7  7 0x7ffdaf695448  0x648f6a2b5ba0 'Foo Bar'

Rust ownership - borrow String

  • &}

  • We can tell Rust that a variable borrows the ownership.

  • In this case both variables have (read) access to the variable.

  • We can have as many (read) borrows as we need.

fn main() {
    let x = String::from("Foo Bar");
    println!("{x}");
    let y = &x;
    println!("{y}");
    println!("{x}");
    println!("{y}");
}

Pass ownership of String to function

fn main() {
    let name = String::from("Foo Bar");
    println!("{name}");

    greet(name);

    // We cannot use name any more as it was moved
    //println!("{name}"); // borrow of moved value: `name`
    //greet(name);        // use of moved value: `name`
}

fn greet(text: String) {
    println!("Greet: {text}");
}
Foo Bar
Greet: Foo Bar

Borrow String when passing to a function

  • &}

  • When passing the variable we need to prefix it with &.

  • In the function definition we also include the & in-front of the type.

  • Inside the function we can prefix it with * to dereference the variable but in general we don't need to as Rust figures that out.

fn main() {
    let name = String::from("Foo Bar");
    println!("{name}");
    greet(&name);
    println!("{name}");
}

fn greet(text: &String) {
    println!("Greet: {}", *text); // explicit derefernce
    println!("Greet: {}", text); // automatic dereference
    println!("Greet: {text}");
}
Foo Bar
Greet: Foo Bar
Greet: Foo Bar
Greet: Foo Bar
Foo Bar

Borrow &str when passing String to a function

fn main() {
    let name = String::from("Foo Bar");
    println!("{name}");
    greet(&name);
    println!("{name}");
}

fn greet(text: &str) {
    println!("Greet: {text}");
}
Foo Bar
Greet: Foo Bar
Foo Bar

Rust function to change string

fn main() {
    let mut text = String::from("Foo");
    println!("Main: {text}");
    display(&text);
    change(&mut text);
    println!("Main: {text}");
    display(&text);
}

fn display(txt: &str) {
    println!("Display: {txt}")
}

fn change(txt: &mut String) {
    //*txt = String::from("Bar");
    txt.push_str(" Bar");
}
Main: Foo
Display: Foo
Main: Foo Bar
Display: Foo Bar

Rust function to change integer (i32)

fn main() {
    let mut counter = 0;
    println!("Main: {counter}");
    display(counter); // value is copy-ed
    println!("Main: {counter}");
    increment(&mut counter);
    display(counter);
    println!("Main: {counter}");
}

fn display(cnt: i32) {
    println!("Display: {cnt}")
}

fn increment(cnt: &mut i32) {
    *cnt += 1;
}
fn main() {
    ex1();
    ex2();
    ex3();
}

fn ex1() {
    let text = "abc";
    println!("{text}");
    let other = text;
    println!("{text}");
    println!("{other}");
    println!();
}

fn ex2() {
    let text = String::from("abc");
    println!("{text}");
    let other = text;
    //println!("{text}");  // borrow of moved value: `text`
    println!("{other}");
    println!();
}

fn ex3() {
    let text = String::from("abc");
    println!("{text}");
    let other = text.clone();
    println!("{text}");
    println!("{other}");
    println!();
}

Lifetime annotation

fn main() {
    let a = "hello";
    let b = String::from("world");

    let c = longer(a, b.as_str());

    println!("c: {}", c);
}

fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
c: world

Change vector of structs

fn main() {
    let mut graph = get_graph();
    dbg!(&graph);
    move_graph(&mut graph);
    dbg!(&graph);
}

fn get_graph() -> Vec<Point> {
    vec![
        Point { x: 0, y: 0 },
        Point { x: 10, y: 20 },
        Point { x: 30, y: 40 },
    ]
}

fn move_graph(graph: &mut Vec<Point>) {
    for point in graph {
        point.x += 100;
    }
}

#[derive(Debug)]
#[allow(dead_code)]
struct Point {
    x: i32,
    y: i32,
}
[examples/ownership/change_vector_of_structs.rs:3] &graph = [
    Point {
        x: 0,
        y: 0,
    },
    Point {
        x: 10,
        y: 20,
    },
    Point {
        x: 30,
        y: 40,
    },
]
[examples/ownership/change_vector_of_structs.rs:5] &graph = [
    Point {
        x: 100,
        y: 0,
    },
    Point {
        x: 110,
        y: 20,
    },
    Point {
        x: 130,
        y: 40,
    },
]

Try to return &str from function

macro_rules! prt {
    ($var:expr) => {
        println!(
            "{:2} {:2} {:p} {:15?} '{}'",
            $var.len(),
            $var.capacity(),
            &$var,
            $var.as_ptr(),
            $var
        );
    };
}

fn main() {
    let res = read_file("cat.txt");
    println!("{:?}", res);
    prt!(res);
}

// fn read_file(file: &str) -> &str {
//     let data = std::fs::read_to_string(file).unwrap();
//     let short = data.trim_end();
//     short
//     // cannot return value referencing local variable `data`
//     // returns a value referencing data owned by the current function
// }

fn read_file(file: &str) -> String {
    let data = std::fs::read_to_string(file).unwrap();
    prt!(data);
    let short = data.trim_end();
    //prt!(short);
    short.to_owned() // this to_owned also copies the data
}

Exercise: concatenate file content

  • TODO

Write a function that received the name of two files and returns the content of the two files concatenated in both directions.

e.g. If file 1 contains "cat" and file 2 contains "dog" then return "catdog" and "dogcat"

fn main() {
    let res = concatenate("cat.txt", "dog.txt");
    println!("{:?} {:?}", res.0, res.1);
}

fn concatenate(file1: &str, file2: &str) -> (String, String) {
    let data1 = std::fs::read_to_string(file1).unwrap();
    let data1 = data1.trim_end();
    let data2 = std::fs::read_to_string(file2).unwrap();
    let data2 = data2.trim_end();
    (format!("{data1}{data2}"), format!("{data2}{data1}"))
}
  • TODO: This works, but let's also implement without format!
  • TODO: Return Result

Pass vector to function

  • TODO
macro_rules! prt {
    ($var:expr) => {
        println!(
            "{:2} {:2} {:p} {:15?} '{:?}'",
            $var.len(),
            $var.capacity(),
            &$var,
            $var.as_ptr(),
            $var
        );
    };
}

fn main() {
    let numbers = vec![5, 2, 3];
    prt!(numbers);
    show_vector_ref(&numbers);
    prt!(numbers);
}

// fn move_vector() {
//     let numbers = vec![5, 2, 3];
//     prt!(numbers);
//     show_vector(numbers);
//     //prt!(numbers);
// }
//
// fn show_vector(things: Vec<i32>) {
//     prt!(things);
// }

fn show_vector_ref(things: &Vec<i32>) {
    prt!(things);
}
 3  3 0x7ffcd0347100  0x5f82d1647b80 '[5, 2, 3]'
 3  3 0x7ffcd0346ad8  0x5f82d1647b80 '[5, 2, 3]'
 3  3 0x7ffcd0347100  0x5f82d1647b80 '[5, 2, 3]'

Date and Time

Measure elapsed time

  • Instant

  • now

  • elapsed

  • std::time::Instant::now

  • as_secs

  • as_millis

  • std::time

  • The Instant type allows us to get snapshots if time that can help us measure elapesd time.

  • In this example we have a function to decide if a number is a prime number. We only use it to have some code that can take substantial time.

  • We get the timestamp before and after and we calculate the elapsed time.

fn main() {
    let start = std::time::Instant::now();
    for number in 2..=100000 {
        let _prime = is_prime(number);
        //println!("{} {}", number, prime);
    }
    let end = std::time::Instant::now();

    println!("{:?}", start);
    println!("{:?}", end);
    let elapsed = end - start;
    println!("{:?}", elapsed);
    println!("{:?}", start.elapsed());
    println!("milliseconds: {:?}", start.elapsed().as_millis());
    println!("seconds:      {:?}", start.elapsed().as_secs());
}

fn is_prime(number: u32) -> bool {
    for div in 2..number {
        if number % div == 0 {
            return false;
        }
    }
    true
}
Instant { tv_sec: 5094523, tv_nsec: 863978009 }
Instant { tv_sec: 5094525, tv_nsec: 872085539 }
2.00810753s
2.008157397s
milliseconds: 2008
seconds:      2

Duration

  • Duration

  • std::time::Duration

  • We can use the Duration type to represent a span of time.

  • std::time::Duration

fn main() {
    let duration = std::time::Duration::from_secs(3);
    println!("as is       {:?}", duration);
    println!("as seconds: {:?}", duration.as_secs());
    println!("as millis:  {:?}", duration.as_millis());
    println!("as micros:  {:?}", duration.as_micros());
    println!("as nanos:   {:?}", duration.as_nanos());
    println!();

    let dur = std::time::Duration::new(2, 4);
    println!("{:?}", dur);
    println!("as seconds: {:?}", dur.as_secs());
    println!();

    let d = duration + dur;
    println!("{:?}", d);

    let d = duration - dur;
    println!("{:?}", d);
}
as is       3s
as seconds: 3
as millis:  3000
as micros:  3000000
as nanos:   3000000000

2.000000004s
as seconds: 2

5.000000004s
999.999996ms

Instant sleep Duration

  • Instant
fn main() {
    let start = std::time::Instant::now();
    println!("{:?}", start);
    std::thread::sleep(std::time::Duration::from_secs(1));
    let end = std::time::Instant::now();
    println!("{:?}", end);
    let elapsed = end - start;
    println!("{:?}", elapsed);
    println!("{:?}", start.elapsed());
}
Instant { tv_sec: 5093638, tv_nsec: 944727638 }
Instant { tv_sec: 5093639, tv_nsec: 944974927 }
1.000247289s
1.000311683s

Handle time (using the time crate)

[package]
name = "handle-time"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
time = "0.3"
//use time::PrimitiveDateTime as DateTime;
use time::Date;
use time::Month;

//    DateTime::new(
//        Date::from_calendar_date(year, month.try_into().unwrap(), day).unwrap(),
//        Time::from_hms(hour, minute, second).unwrap(),
//    )

fn main() {
    let date = Date::from_calendar_date(2023, Month::June, 18);
    println!("{:?}", date);

    one();
    two();
    three();

    other();
}

fn one() {
    let year = 2023;
    let month = 6;
    let day = 18;
    let date = Date::from_calendar_date(year, month.try_into().unwrap(), day).unwrap();
    print_date(date);
}

fn two() {
    let year = 2023;
    let month = Month::June;
    let day = 18;
    let date = Date::from_calendar_date(year, month, day).unwrap();
    print_date(date);
}

fn three() {
    let year = 2023;
    let month = 6;
    let month: Month = month.try_into().unwrap();
    let day = 18;
    let date = Date::from_calendar_date(year, month, day).unwrap();
    print_date(date);
}

fn other() {
    let date = Date::from_calendar_date(2023, Month::June, 18).unwrap();
    println!("{:?}", date.midnight());
}

fn print_date(date: Date) {
    println!("{:?}", date);
    println!("{}", date.year());
    println!("{}", date.month());
    println!("{}", date.day());
    println!();
}

Modules

Function in the main.rs file

  • We already know that we can define functions in the main.rs file and call them.
fn main() {
    println!("main");
    in_main();
}

fn in_main() {
    println!("in_main");
}
main
in_main

Module defined in the main.rs file

  • mod

  • pub

  • In order to divide our code into logical units we can use modules.

  • First step is to define a module using the mod keyword in the main.rs file and inside the module we can define functions, structs, enums etc.

  • However, in order to be able to call the function from the code outside of the module, we need to make the function public using the pub keyword.

  • We can access the public function in two different ways.

fn main() {
    println!("in main");

    // crate::tools::private_helper(); // private function

    tools::public_helper();
    crate::tools::public_helper();
}

mod tools {
    fn private_helper() {
        println!("in tools::private_helper");
    }

    pub fn public_helper() {
        println!("in tools::public_helper");

        private_helper();
    }
}
in main
in tools::public_helper
in tools::private_helper
in tools::public_helper
in tools::private_helper

Module in other file

  • mod
  • pub
fn main() {
    println!("in main");

    // crate::tools::private_helper(); // private function

    tools::public_helper();
    crate::tools::public_helper();
}

mod tools;
#![allow(unused)]
fn main() {
fn private_helper() {
    println!("in tools::private_helper");
}

pub fn public_helper() {
    println!("in tools::public_helper");

    private_helper();
}
}
in main
in tools::public_helper
in tools::private_helper
in tools::public_helper
in tools::private_helper

Modules and enums

  • Internally in the helper module we can use the enum.
  • If we return it in a public function (leaking it) we get a compiler warning.
  • If we try to use the public function in the outside world, we get a compile error.
  • We need to declare the enum to be pub. Then all its variants also become public.
fn main() {
    helper::show_animal();

    let animal = helper::get_animal();
    println!("{:?}", animal);
}

mod helper {

    #[allow(dead_code)]
    #[derive(Debug)]
    pub enum Animal {
        Cat,
        Dog,
    }

    pub fn show_animal() {
        let animal = Animal::Cat;
        println!("{:?}", animal);
    }

    pub fn get_animal() -> Animal {
        Animal::Dog
    }
}

Modules and structs

fn main() {
    tools::use_struct();

    let p = tools::get_struct();
    println!("after get_struct: {:#?}", p);
    assert_eq!(p.fname, "Public");
    //println!("Hello {}", p.lname); // private field
}

mod tools {
    #[allow(dead_code)]
    #[derive(Debug)]
    pub struct Person {
        pub fname: String,
        lname: String,
    }

    pub fn use_struct() {
        let p = Person {
            fname: String::from("Foo"),
            lname: String::from("Bar"),
        };

        println!("in use_struct: {:#?}", p);
        assert_eq!(p.fname, "Foo");
        assert_eq!(p.lname, "Bar");
    }

    pub fn get_struct() -> Person {
        #[allow(clippy::let_and_return)]
        let p = Person {
            fname: String::from("Public"),
            lname: String::from("User"),
        };
        p
    }
}

Crates

Create a crate

  • TODO

  • Where do we record changes between releases?

Sets

HashSet

  • HashSet
  • sets are implemented as Hashes with the value being ().

Basic Set operations in Rust

  • set

  • HashSet

  • insert

  • remove

  • contains

  • A HashSet can be used for the mathematical SET operations.

  • We can insert values. The HashSet will only contain one copy of each value regardless the number of times we insert it.

  • We can remove values.

  • We can get the number of elements in the set using len.

  • We can check if a set contains a certain value.

  • There is no order among the elements so when we print them they might be in any order.

use std::collections::HashSet;

fn main() {
    let mut english: HashSet<String> = HashSet::new();
    println!("{:?}", &english);
    assert_eq!(english.len(), 0);
    assert_eq!(format!("{:?}", &english), "{}");

    english.insert(String::from("chair"));
    println!("{:?}", &english);
    assert_eq!(english.len(), 1);
    assert_eq!(format!("{:?}", &english), r#"{"chair"}"#);

    english.insert(String::from("table"));
    println!("{:?}", &english);
    assert_eq!(english.len(), 2);
    assert!(
        format!("{:?}", &english) == r#"{"table", "chair"}"#
            || format!("{:?}", &english) == r#"{"chair", "table"}"#
    );

    english.insert(String::from("chair"));
    println!("{:?}", &english);
    assert_eq!(english.len(), 2);
    assert!(
        format!("{:?}", &english) == r#"{"table", "chair"}"#
            || format!("{:?}", &english) == r#"{"chair", "table"}"#
    );

    assert!(english.contains("chair"));
    assert!(!english.contains("door"));
    assert_eq!(english.len(), 2);

    println!("----");
    for word in &english {
        println!("{}", word);
    }
    println!("----");

    english.remove("chair");
    println!("{:?}", &english);
    assert_eq!(english.len(), 1);
    assert_eq!(format!("{:?}", &english), r#"{"table"}"#);
}
{}
{"chair"}
{"chair", "table"}
{"chair", "table"}
----
chair
table
----
{"table"}

Union and bitor

  • union
  • bitor
use std::collections::HashSet;

fn main() {
    let mut english: HashSet<&str> = HashSet::new();
    let mut spanish: HashSet<&str> = HashSet::new();

    for word in ["door", "car", "lunar", "era"] {
        english.insert(word);
    }
    for word in ["era", "lunar", "hola"] {
        spanish.insert(word);
    }

    println!("{:?}", &english);
    println!("{:?}", &spanish);

    assert_eq!(english.len(), 4);
    assert_eq!(spanish.len(), 3);

    assert!(english.contains("door"));
    assert!(english.contains("era"));
    assert!(!english.contains("hola"));

    assert!(spanish.contains("hola"));

    let both_by_bitor = &english | &spanish;
    println!("{:?}", both_by_bitor);

    assert!(both_by_bitor.contains("door"));
    assert!(both_by_bitor.contains("era"));
    assert!(both_by_bitor.contains("hola"));

    // This does not change the original sets
    assert!(english.contains("door"));
    assert!(english.contains("era"));
    assert!(!english.contains("hola"));

    // Returns a https://doc.rust-lang.org/std/collections/hash_set/struct.Union.html that could be
    // collect-ed
    let both = &english.union(&spanish);
    println!("{:?}", both);
}
{"door", "car", "lunar", "era"}
{"hola", "era", "lunar"}
{"car", "hola", "lunar", "door", "era"}
["door", "car", "lunar", "era", "hola"]

Difference

  • difference
use std::collections::HashSet;

fn main() {
    let mut english: HashSet<String> = HashSet::new();
    let mut spanish: HashSet<String> = HashSet::new();

    for word in ["door", "car", "lunar", "era"] {
        english.insert(word.to_owned());
    }
    for word in ["era", "lunar", "hola"] {
        spanish.insert(word.to_owned());
    }

    println!("{:?}", &english);
    println!("{:?}", &spanish);

    println!("{:?}", english.difference(&spanish));
    println!("{:?}", spanish.difference(&english));
}
{"lunar", "era", "door", "car"}
{"hola", "era", "lunar"}
["door", "car"]
["hola"]

Lifetime

Lifetime elision rules

rustc --explain E0106

Function receiving and returning one reference

  • the elision rules apply and thus we don't need lifetime specifiers
fn main() {
    let mut x = String::new();
    println!("{x}");
    x = String::from("before");
    println!("{x}");
    let c = select(&x);
    x = String::from("after");

    println!("{}", c);
    println!("{}", x);
}

fn select(text: &str) -> &'static str {
    if text > "abc" {
        "first"
    } else {
        "second"
    }
}

// fn select(text: &str) -> &str {
//     // fn select<'a>(text: &'a str) -> &'a str {  // the same as above
//     // fn select<'a, 'b>(text: &'a str) -> &'b str { // fully generic
//     // fn select<'b>(text: &str) -> &'b str { // specific lifetime
//     // fn select(text: &str) -> &'static str {
//         if text > "abc" {
//             "first"
//         } else {
//             "second"
//         }
//     }
Foo
Foo

Function receiving two and returning one reference

#![allow(dead_code)]

fn main() {
    let mut a = String::new();
    let mut b = String::new();
    println!("a {a}");
    println!("b {b}");

    a = String::from("AAA");
    b = String::from("BBB");

    let c = select(&a, &b);
    //a = String::from("aaa");
    //b = String::from("bbb");

    println!("c {c}");
    println!("a {a}");
    println!("b {b}");
}

fn select(name1: &str, name2: &str) -> String {
    if name1 > name2 {
        String::from("first")
    } else {
        String::from("second")
    }
}

// fn select<'a>(name1: &'a str, name2: &str) -> &'a str {
//     if name1 > name2 {
//         name1
//     } else {
//         "ab"
//     }
// }

// fn select<'a>(name1: &str, name2: &str) -> &'a str {
//     if name1 > name2 {
//         "first"
//     } else {
//         "second"
//     }
// }

// fn select(name1: &str, name2: &str) -> &'static str {
//     if name1 > name2 {
//         "first"
//     } else {
//         "second"
//     }
// }
Foo
Foo

Exercise: longest string

  • Implement a function that received 3 strings and returns the longest string
fn longest(a: &str, b: &str, c: &str) -> &str
  • You will probably need some lifetime annotations.

Exercise: lifetime

  • Implement a function that receives 2 strings and returns one string.
  • If the first string is longer than the second string return the first string.
  • Otherwise return the string "nope".

Iterators

Iterate over vector of numbers

  • vec!
  • for
fn main() {
    let numbers = vec![1, 2, 3];

    println!("{:?}", numbers);

    for n in &numbers {
        println!("{}", n);
    }
    println!("{:?}", numbers);
}
[1, 2, 3]
1
2
3
[1, 2, 3]

Alternatively, we could create an iterator using the iter method

  • iter
fn main() {
    let numbers = vec![1, 2, 3];

    println!("{:?}", numbers);
    for n in numbers.iter() {
        println!("{}", n);
    }
    println!("{:?}", numbers);
}
[1, 2, 3]
1
2
3
[1, 2, 3]

Count number of elements of an iterator

  • count
fn main() {
    let numbers = [3, 4, 5];
    println!("{}", numbers.len());
    println!("{}", numbers.iter().count());

    // An iterator
    let readdir = std::path::Path::new(".").read_dir().unwrap();
    println!("{:?}", readdir);
    println!("{}", readdir.count());
}
3
3
ReadDir(".")
5

Iterator: all the elements

  • all

  • iter

  • into_iter

  • all - calls a closure on every element of the iterator and if the closure returns true for every element then the expression returns true.

  • See the documentation of the all method.

  • iter iterates over borrowed references and thus we need to dereference the variables with *, but we can continue useing the original vector.

  • into_iter iterates over the real values and thus we cannot use the original vector any more.

  • The last example shows that the iteration will stop when it encounters the first false.

fn main() {
    let numbers = vec![23, 7, 12, 8];

    if numbers.iter().all(|num| *num > 0) {
        println!("Positive numbers");
    }
    if numbers.iter().all(|num| *num >= 10) {
        println!("Double digit numbers");
    }

    if numbers.into_iter().all(|num| num > 0) {
        println!("Positive numbers");
    }

    // use of moved value: `numbers`
    // if numbers.into_iter().all(|num| num >= 10) {
    //     println!("Double digit numbers");
    // }

    let numbers = vec![23, 12, 7, 8];

    if numbers.iter().all(big) {
        println!("Double digit numbers");
    }

    if numbers.into_iter().all(|num| {
        println!("Checking {num}");
        num >= 10
    }) {
        println!("Double digit numbers");
    }
}

fn big(num: &i32) -> bool {
    println!("Checking in big {num}");
    *num >= 10
}
Positive numbers
Positive numbers
Checking 23
Checking 12
Checking 7

Iteration moves values

  • If we remove the & from the first iteration the code won't compile any more as we have moved the values.
fn main() {
    let animals = vec![
        String::from("cat"),
        String::from("dog"),
        String::from("crab"),
    ];

    for animal in &animals {
        println!("{animal}");
    }
    println!();

    for animal in animals {
        println!("{animal}");
    }
}
cat
dog
crab

cat
dog
crab

Iterator that restarts

  • next

  • After consuming all the elements of the iterator it return None, but then it restarts and we can ask for the next element.

struct CircleOfLife {
    things: Vec<String>,
    index: Option<usize>,
}

impl CircleOfLife {
    fn new(things: &[String]) -> Self {
        CircleOfLife {
            things: things.to_owned(),
            index: None,
        }
    }
}

impl Iterator for CircleOfLife {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        if self.index.is_none() {
            self.index = Some(0);
        } else {
            let index = self.index.unwrap();
            self.index = Some(index + 1)
        }

        let index = self.index.unwrap();
        if index >= self.things.len() {
            self.index = None;
            return None;
        }

        Some(self.things[index].clone())
    }
}

fn main() {
    let animals = vec![
        String::from("cat"),
        String::from("dog"),
        String::from("crab"),
    ];

    let mut iterator = CircleOfLife::new(&animals);

    loop {
        if let Some(animal) = iterator.next() {
            println!("{animal}")
        } else {
            println!("No more animals");
            break;
        }
    }

    if let Some(animal) = iterator.next() {
        println!("{animal}")
    } else {
        println!("This iterator is finished");
    }
}
cat
dog
crab
No more animals
cat

Circular Iterator that restarts

  • next
struct CircleOfLife {
    things: Vec<String>,
    index: Option<usize>,
}

impl CircleOfLife {
    fn new(things: &[String]) -> Self {
        CircleOfLife {
            things: things.to_owned(),
            index: None,
        }
    }
}

impl Iterator for CircleOfLife {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        if self.index.is_none() {
            self.index = Some(0);
        } else {
            let index = self.index.unwrap();
            if index + 1 >= self.things.len() {
                self.index = Some(0);
            } else {
                self.index = Some(index + 1)
            }
        }

        let index = self.index.unwrap();
        Some(self.things[index].clone())
    }
}

fn main() {
    let animals = vec![
        String::from("cat"),
        String::from("dog"),
        String::from("crab"),
    ];

    let mut iterator = CircleOfLife::new(&animals);

    for _ in 1..=5 {
        if let Some(animal) = iterator.next() {
            println!("{animal}")
        } else {
            println!("No more animals");
            break;
        }
    }
}
cat
dog
crab
cat
dog

Create a simple iterator to count up to a number

  • Iterator
  • Item
  • next
fn main() {
    let mut cnt = tools::Counter::new(5);
    //let cnt = tools::Counter::new_till42();
    // let cnt = tools::Counter {
    //     current: 3,
    //     limit: 7,
    // };
    println!("{:?}", &cnt);
    // for x in cnt {
    //     println!("{}", x);
    // }

    loop {
        match cnt.next() {
            Some(val) => println!("{val}"),
            None => {
                println!("done");
                break;
            }
        }
    }
}

mod tools {
    #[derive(Debug)]
    pub struct Counter {
        current: u32,
        limit: u32,
    }

    impl Counter {
        pub fn new(limit: u32) -> Counter {
            Counter { current: 0, limit }
        }
        // pub fn new_till42() -> Counter {
        //     Counter { current: 0, limit: 42 }
        // }
    }

    impl Iterator for Counter {
        type Item = u32;

        fn next(&mut self) -> Option<Self::Item> {
            self.current += 1;
            if self.current > self.limit {
                return None;
            }
            Some(self.current)
        }
    }
}
Counter { current: 0, limit: 5 }
1
2
3
4
5

Create a simple iterator to count boundless

  • break
#[derive(Debug)]
//#[allow(dead_code)]
struct Counter {
    current: u8,
}

impl Counter {
    fn new() -> Counter {
        Counter { current: 0 }
    }
}

impl Iterator for Counter {
    type Item = u8;

    fn next(&mut self) -> Option<Self::Item> {
        self.current += 1;
        Some(self.current)
    }
}

fn main() {
    let cnt = Counter::new();
    println!("{:?}", &cnt);
    for x in cnt {
        println!("{}", x);
        //if 10 <= x {
        //    break;
        //}
    }
}
Counter { current: 0 }
1
2
3
4
5
6
7
8
9
10

Iterate over files in current directory

  • read_dir
use std::path::Path;

fn main() {
    let path_from_user = ".";
    let path = Path::new(path_from_user);
    match path.read_dir() {
        Ok(dh) => {
            // A ReadDir instance
            for entry in dh {
                println!("{:?}", entry);
            }
        }
        Err(err) => println!("Could not open directory '{}': {}", path_from_user, err),
    }
}

Iterate over files in current directory calling next

  • read_dir
  • next
use std::path::Path;

fn main() {
    let args = std::env::args().collect::<Vec<_>>();
    if args.len() != 2 {
        eprintln!("Usage: {} PATH", args[0]);
        std::process::exit(1);
    }

    list_dir(&args[1]);
}

fn list_dir(path_from_user: &str) {
    let path = Path::new(path_from_user);

    match path.read_dir() {
        Ok(mut dh) => {
            // A ReadDir instance
            loop {
                let entry = dh.next();
                match entry {
                    Some(value) => println!("{:?}", value),
                    None => break,
                }
            }
        }
        Err(err) => println!("Could not open directory '{}': {}", path_from_user, err),
    }
}

Iterator to walk directory tree

  • ReadDir
use std::fs::ReadDir;
use std::path::Path;

//#[derive(Debug)]
//#[allow(dead_code)]
struct Walk {
    rds: Vec<ReadDir>,
}

impl Walk {
    fn new(root: &str) -> Result<Walk, Box<dyn std::error::Error>> {
        let path = Path::new(root);
        match path.read_dir() {
            Ok(rd) => {
                let w = Walk { rds: vec![rd] };
                std::result::Result::Ok(w)
            }
            Err(err) => {
                println!("Could not open dir '{}': {}", root, err);
                Err(Box::new(err))
            }
        }
    }
}

impl Iterator for Walk {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        if self.rds.is_empty() {
            //println!("empty rds");
            return None;
        }
        let count = self.rds.len();
        let entry = self.rds[count - 1].next();
        match entry {
            Some(result) => match result {
                Ok(dir_entry) => {
                    if dir_entry.path().is_dir() {
                        match dir_entry.path().read_dir() {
                            Ok(rd) => {
                                self.rds.push(rd);
                            }
                            Err(err) => {
                                println!("Could not open dir {}", err);
                            }
                        }
                    }
                    return Some(dir_entry.path().to_str().unwrap().to_string());
                }
                Err(_err) => None,
            },
            None => {
                self.rds.pop();
                self.next()
            }
        }
    }
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    if args.len() != 2 {
        eprintln!("Usage: {} PATH", args[0]);
        std::process::exit(1);
    }
    let path_from_user = &args[1];
    let tree = match Walk::new(path_from_user) {
        Ok(tree) => tree,
        Err(err) => {
            println!("Error: {}", err);
            std::process::exit(1);
        }
    };
    //println!("{:?}", &tree);
    for entry in tree {
        println!("{:?}", entry);
    }
}

Mutable iterator

  • iter_mut
fn main() {
    let mut numbers = [2, 3, 4];
    println!("{numbers:?}");

    for num in numbers.iter_mut() {
        *num += 1;
    }

    println!("{numbers:?}");
}
[2, 3, 4]
[3, 4, 5]

Take the first N elements of an iterator

  • take

  • take creates a new iterator from the first n element of any iterator.

  • It can be used on an infinite iterator as well.

  • The example we have here does not add value over just using a range, but for every other iterator it will make sense.

fn main() {
    let n = 5;
    let taken = (0..).take(n).collect::<Vec<_>>();
    let range = (0..n).collect::<Vec<_>>();
    assert_eq!(taken, range);
    println!("{:?}", taken);

    // If there are not enough iterations it stops when the iterator is exhausted
    let numbers = range.iter().take(10).collect::<Vec<_>>();
    println!("{:?}", numbers);
}
[0, 1, 2, 3, 4]
[0, 1, 2, 3, 4]

Skip the first N elements of an iterator

  • skip
fn main() {
    let skipped = (0..10).skip(5).collect::<Vec<_>>();
    let range = (5..10).collect::<Vec<_>>();
    assert_eq!(skipped, range);
    println!("{:?}", skipped);

    // If we exhaust the iterator while skipping, that's fine. We get an empty iterator.
    let numbers = (0..5).skip(10).collect::<Vec<_>>();
    println!("{:?}", numbers);
}
[5, 6, 7, 8, 9]
[]

Skip and take from an iterator

  • skip
  • take
fn main() {
    let taken = (0..).skip(5).take(7).collect::<Vec<_>>();
    let range = (5..12).collect::<Vec<_>>();
    assert_eq!(taken, range);
    println!("{:?}", taken);
}
[5, 6, 7, 8, 9, 10, 11]

Counting the number of iterations - count vs collect-len

#[allow(dead_code)]
#[derive(Debug)]
struct Number {
    num: u32,
}
impl Number {
    fn new(num: u32) -> Self {
        println!("Creating {num}");
        Self { num }
    }
}

impl Drop for Number {
    fn drop(&mut self) {
        println!("Dropping Number! {}", self.num);
    }
}

struct Range {
    start: u32,
    end: u32,
}

impl Range {
    fn new(start: u32, end: u32) -> Self {
        Self { start, end }
    }
}

impl Iterator for Range {
    type Item = Number;

    fn next(&mut self) -> Option<Self::Item> {
        if self.end < self.start {
            return None;
        }

        let current = self.start;
        self.start += 1;

        Some(Number::new(current))
    }
}

fn main() {
    let mut counter = 0;
    let range = Range::new(3, 5);
    for num in range {
        counter += 1;
        println!("{num:?}");
    }
    println!("for loop: {counter}");

    println!("-----");
    println!(
        "collect.len: {}",
        Range::new(3, 5).collect::<Vec<_>>().len()
    );

    println!("-----");
    println!("count: {}", Range::new(3, 5).count());
}
Creating 3
Number { num: 3 }
Dropping Number! 3
Creating 4
Number { num: 4 }
Dropping Number! 4
Creating 5
Number { num: 5 }
Dropping Number! 5
for loop: 3
-----
Creating 3
Creating 4
Creating 5
collect.len: 3
Dropping Number! 3
Dropping Number! 4
Dropping Number! 5
-----
Creating 3
Dropping Number! 3
Creating 4
Dropping Number! 4
Creating 5
Dropping Number! 5
count: 3

Exercise: Iterator for the Fibonacci series

  • Crate an iterator that on every iteration will return the next value from the Fibonacci series.

Solution: Iterator for the Fibonacci series

#[derive(Debug)]
struct Fibonacci {
    index: u32,
    current: u32,
    previous: u32,
}

impl Fibonacci {
    fn new() -> Fibonacci {
        Fibonacci {
            index: 0,
            current: 0,
            previous: 0,
        }
    }
}

impl Iterator for Fibonacci {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.index == 0 {
            self.index = 1;
            self.current = 1;
        } else if self.index == 1 {
            self.index = 2;
            self.current = 1;
            self.previous = 1;
        } else {
            (self.current, self.previous) = (self.current + self.previous, self.current);
        }

        Some(self.current)
    }
}

fn main() {
    let fibonacci = Fibonacci::new();
    println!("{:?}", &fibonacci);
    for fibo in fibonacci {
        println!("{fibo}");
        if 100 <= fibo {
            break;
        }
    }
}

Iter strings

  • TODO
macro_rules! prt {
    ($var: expr) => {
        println!(
            "{:p} {:?} {} {} {}",
            &$var,
            $var.as_ptr(),
            $var.len(),
            $var.capacity(),
            $var
        );
    };
}
fn main() {
    let animals = vec![
        String::from("cat"),
        String::from("dog"),
        String::from("fish"),
        String::from("crab"),
        String::from("mouse"),
        String::from("moose"),
    ];

    println!(
        "{}",
        animals.iter().all(|animal| {
            prt!(animal);
            !animal.is_empty()
        })
    );
    println!();

    // println!("{}", animals.into_iter().all(|animal| {
    //     prt!(animal);
    //     animal.len() > 0
    // }));

    // // This is like into_iter, it moves the animals
    // for animal in animals {
    //     prt!(animal);
    // }

    // pluck out two values and take ownership of them so we
    // won't be able to use animals any more.
    // let short = animals.into_iter().take(2).collect::<Vec<_>>();
    // for animal in short {
    //     prt!(animal);
    // }
    // println!();

    let short = animals.iter().take(2).clone().collect::<Vec<_>>();
    for animal in short {
        prt!(animal);
    }
    println!();

    let short = animals
        .iter()
        .take(2)
        .map(|thing| {
            println!("cloning");
            thing.clone()
        })
        .collect::<Vec<_>>();
    for animal in short {
        prt!(animal);
    }
    println!();

    let short = animals.iter().take(2).cloned().collect::<Vec<_>>();
    for animal in short {
        prt!(animal);
    }
    println!();

    for animal in animals {
        prt!(animal);
    }
}

Non-circular iterators

  • TODO
fn main() {
    let animals = [
        String::from("cat"),
        String::from("dog"),
        String::from("crab"),
    ];

    let mut iterator = animals.iter();
    loop {
        if let Some(animal) = iterator.next() {
            println!("{animal}")
        } else {
            println!("No more animals");
            break;
        }
    }

    if let Some(animal) = iterator.next() {
        println!("{animal}")
    } else {
        println!("This iterator is finished");
    }
}
cat
dog
crab
No more animals
This iterator is finished

Advanced Functions

Pass function as argument - hello world

pass function as a parameter to another function

fn main() {
    //hello();
    call(hello);
    call(world);
}

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

fn world() {
    println!("World");
}

fn call(my_function: fn() -> ()) {
    my_function();
}
Hello
World

Pass function with parameter as an argument

fn main() {
    call(hello);
}

fn hello(text: &str) {
    println!("Hello {}", text);
}

fn call(my_function: fn(text: &str) -> ()) {
    my_function("Foo");

    my_function("Bar");
}
Hello Foo
Hello Bar

Dispatch table - Calculator

use std::collections::HashMap;

fn add(x: i32, y: i32) -> i32 {
    x + y
}

fn multiply(x: i32, y: i32) -> i32 {
    x * y
}

fn main() {
    let table = {
        let mut table: HashMap<char, fn(i32, i32) -> i32> = HashMap::new();
        table.insert('+', add);
        table.insert('*', multiply);
        table.insert('-', |x, y| x - y);
        table.insert('/', |x, y| x / y);
        table
    };

    for op in ['+', '*', '-', '/'] {
        let res = table[&op](8, 2);
        println!("8 {op} 2 = {res}");
    }
}
8 + 2 = 10
8 * 2 = 16
8 - 2 = 6
8 / 2 = 4

Dispatch table - Calculator

use std::collections::HashMap;

fn add(x: i32, y: i32) -> i32 {
    x + y
}

fn multiply(x: i32, y: i32) -> i32 {
    x * y
}

fn main() {
    let table: HashMap<char, fn(i32, i32) -> i32> = HashMap::from([
        ('+', add as fn(i32, i32) -> i32),
        ('*', multiply),
        ('-', |x, y| x - y),
        ('/', |x, y| x / y),
    ]);

    for op in ['+', '*', '-', '/'] {
        let res = table[&op](8, 2);
        println!("8 {op} 2 = {res}");
    }
}
8 + 2 = 10
8 * 2 = 16
8 - 2 = 6
8 / 2 = 4

See https://users.rust-lang.org/t/mismatched-types-when-creating-dispatch-table/114527.

Generic functions to add numbers

  • Generics
fn main() {
    let a = 7;
    let b = 8;
    println!("{}", add32(a, b));

    let c = 7_i8;
    let d = 29_i8;
    println!("{}", add8(c, d));

    println!("{}", add(a, b));
    println!("{}", add(c, d));
}

fn add32(x: i32, y: i32) -> i32 {
    x + y
}

fn add8(x: i8, y: i8) -> i8 {
    x + y
}

// fn add<T>(x: T, y: T) -> T {
//     print_type(&x);
//     x + y
// }

fn add<T: std::ops::Add<Output = T>>(x: T, y: T) -> T {
    print_type(&x);
    x + y
}

fn print_type<T>(_: &T) {
    println!("{:?}", std::any::type_name::<T>());
}

Generic functions to add numbers using where clause

  • where
fn main() {
    let a = 7;
    let b = 8;

    let c = 7_i8;
    let d = 29_i8;

    println!("{}", add(a, b));
    println!("{}", add(c, d));
}

fn add<T>(x: T, y: T) -> T
where
    T: std::ops::Add<Output = T>,
{
    x + y
}

Exercise: generic function

  • Implement a function that receives two u8 values and returns the bigger.
  • Implement a function that receives two f32 values and returns the bigger.
  • Implement a generic function that receives two values (of the same type) and returns the bigger.

Exercise: call the add function for two points

  • Create a struct representing a point: two attibutes x and y holding u32 both.
  • Call the add function passing two points to it.
  • The result needs to be a new Point that has x1 + x2 and y1 + y2 as coordinates.

Exercise: Implement function to repeate a string

  • Implement a function that receives a reference to a string and an integer number and returns the string repeated N times:
repeat("abc_", 3)  returns abc_abc_abc_

Make sure we can accept any integer.

Solution: Implement function to repeate a string

  • TODO
fn main() {
    println!("{}", repeat("a", 3));
}

fn repeat(txt: &str, n: u32) -> String {
    let mut text = String::new();
    println!("{:?}", n);
    for _ in 1..=n {
        text.push_str(txt);
    }

    text
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn check_repeat() {
        assert_eq!(repeat("1bc_", 3), "1bc_1bc_1bc_");
        // assert_eq!(repeat("2bc_", 3u8), "2bc_2bc_2bc_");
        // assert_eq!(repeat("3bc_", 3u16), "3bc_3bc_3bc_");
    }
}

Solution: call the add function for two points

#[derive(Debug)]
struct Point32 {
    x: u32,
    y: u32,
}

impl std::ops::Add for Point32 {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    let a = 7;
    let b = 8;

    let c = 7_i8;
    let d = 29_i8;

    let p1 = Point32 { x: 2, y: 3 };

    let p2 = Point32 { x: 10, y: 20 };

    println!("{}", add(a, b));
    println!("{}", add(c, d));

    println!("{:?}", add(p1, p2));
}

fn add<T>(x: T, y: T) -> T
where
    T: std::ops::Add<Output = T>,
{
    x + y
}
15
36
Point32 { x: 12, y: 23 }

DateTime with Chrono

Chrono now

use chrono::{DateTime, Local, Utc};

fn main() {
    let utc: DateTime<Utc> = Utc::now();
    println!("utc:           {}", utc);
    println!("utc timestamp: {}", utc.timestamp());
    println!("utc format:    {}", utc.format("%Y-%m-%d"));

    println!();
    let local: DateTime<Local> = Local::now();
    println!("{}", local);
    println!("{}", local.timestamp());

    let text = String::from("2023-08-30T12:30:01+0000");
    let dt = DateTime::parse_from_str(&text, "%Y-%m-%dT%H:%M:%S%z").unwrap();
    println!();
    println!("dt:           {}", dt);
    println!("dt format:    {}", dt.format("%Y-%m-%d"));
    println!("dt format:    {}", dt.format("%H::%M::%S"));
}
[package]
name = "chrono-demo"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chrono = "0.4.26"
utc:           2023-08-31 07:11:59.543230392 UTC
utc timestamp: 1693465919
utc format:    2023-08-31

2023-08-31 10:11:59.543294786 +03:00
1693465919

dt:           2023-08-30 12:30:01 +00:00
dt format:    2023-08-30
dt format:    12::30::01

Elapsed time

[package]
name = "chrono-elapsed-time"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chrono = "0.4.26"
start:             2023-08-18 10:54:06.606602231 UTC
start timestamp:   1692356046
start timestamp:   1692356046606602

elapsed as_micros: 10158
elapsed as_millis: 10

end:               2023-08-18 10:54:06.616858288 UTC
end timestamp:     1692356046
end timestamp:     1692356046616858

Elapsed (sec):     0
Elapsed (millis):  10
Elapsed (micros):  10256
use chrono::{DateTime, Utc};
use std::{thread, time};

fn main() {
    let start: DateTime<Utc> = Utc::now();
    println!("start:             {}", start);
    println!("start timestamp:   {}", start.timestamp());
    println!("start timestamp:   {}", start.timestamp_micros());
    println!();

    let ten_millis = time::Duration::from_millis(10);
    let now = time::Instant::now();

    thread::sleep(ten_millis);
    println!("elapsed as_micros: {}", now.elapsed().as_micros());
    println!("elapsed as_millis: {}", now.elapsed().as_millis());

    let end: DateTime<Utc> = Utc::now();
    println!();
    println!("end:               {}", end);
    println!("end timestamp:     {}", end.timestamp());
    println!("end timestamp:     {}", end.timestamp_micros());

    println!();
    println!("Elapsed (sec):     {}", end.timestamp() - start.timestamp());
    println!(
        "Elapsed (millis):  {}",
        end.timestamp_millis() - start.timestamp_millis()
    );
    println!(
        "Elapsed (micros):  {}",
        end.timestamp_micros() - start.timestamp_micros()
    );
}

Date and time arithmetic

[package]
name = "chrono-date-arithmetic"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chrono = "0.4.26"
use chrono::{DateTime, Duration, Utc};

fn main() {
    let now: DateTime<Utc> = Utc::now();
    println!("{}", now);

    let tomorrow = now + Duration::days(1);
    println!("{}", tomorrow);

    let yesterday = now - Duration::days(1);
    println!("{}", yesterday);

    let two_hours_later = now + Duration::hours(2);
    println!("{}", two_hours_later);
}
2023-09-18 16:22:26.308212333 UTC
2023-09-19 16:22:26.308212333 UTC
2023-09-17 16:22:26.308212333 UTC
2023-09-18 18:22:26.308212333 UTC

Compare chrono dates

[package]
name = "chrono-compare-dates"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chrono = "0.4"
use chrono::{DateTime, Duration, Utc};

fn main() {
    let now: DateTime<Utc> = Utc::now();
    println!("{}", now);

    let later = now + Duration::seconds(1);
    println!("{}", later);

    println!("later is bigger: {}", now < later);
    println!("later is not smaller: {}", now > later);
    println!("later is not the same as now: {}", now == later);
    println!();
    println!("{:?}", now.cmp(&later));
    println!();

    let now2 = later - Duration::seconds(1);
    println!("{}", now2);
    println!("now is now: {}", now == now2);
}
2023-10-20 11:54:30.285843755 UTC
2023-10-20 11:54:31.285843755 UTC
later is bigger: true
later is not smaller: false
later is not the same as now: false

Less

2023-10-20 11:54:30.285843755 UTC
now is now: true

Format DateTime

[package]
name = "chrono-format-date"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chrono = "0.4"
use chrono::{DateTime, Utc};

fn main() {
    let now: DateTime<Utc> = Utc::now();
    println!("{}", now);
    println!("{}", now.format("%Y-%m-%d %H:%M:%S.%f %Z"));
}

Parse string to datetime

  • chrono
  • parse
  • parse_from_str
  • naive_utc
  • with_ymd_and_hms
[package]
name = "chrono-parse-date-string"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chrono = "0.4"
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};

fn main() {
    let ts1 = "2023-09-19T08:00:01Z".parse::<DateTime<Utc>>().unwrap();
    let ts2 = "2023-09-19T08:00:02Z".parse::<DateTime<Utc>>().unwrap();
    println!("{}", ts1 < ts2);

    let dt1 = Utc.with_ymd_and_hms(2023, 9, 19, 8, 0, 1).unwrap();
    println!("{}", ts1 == dt1);

    //let ts3 = DateTime::parse_from_str("2023-09-18", "%Y-%m-%d").unwrap();
    //let ts3 = DateTime::parse_from_str("2023-09-18 01:44:10 ", "%Y-%m-%d %H:%M:%S").unwrap();
    let ts3 = NaiveDateTime::parse_from_str("2023-09-19 08:00:02", "%Y-%m-%d %H:%M:%S").unwrap();
    println!("{}", ts3);

    let ts4 =
        NaiveDateTime::parse_from_str("2023-09-19 08:00:02 UTC", "%Y-%m-%d %H:%M:%S %Z").unwrap();
    println!("{}", ts4);

    println!("{}", ts3 == ts4);

    let ts5 =
        DateTime::parse_from_str("2023-09-19 08:00:02 +00:00", "%Y-%m-%d %H:%M:%S %z").unwrap();
    println!("{}", ts5);

    println!("{}", ts3 == ts5.naive_utc());
}
true
true
2023-09-19 08:00:02
2023-09-19 08:00:02
true
2023-09-19 08:00:02 +00:00
true

Testing

Testing a library crate

  • lib
  • test
$ cargo new --lib test-lib
cd test-lib
cargo test
#![allow(unused)]
fn main() {
pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}
}

Test coverage report with tarpaulin

#![allow(unused)]
fn main() {
pub fn add(left: usize, right: usize) -> usize {
    left + right
}

pub fn multiply(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}
}
cargo install cargo-tarpaulin
cargo tarpaulin

Exclude the test functions from the report:

cargo tarpaulin --ignore-tests
  • Generate HTML report
cargo tarpaulin --ignore-tests -o Html

Test a function in a crate

  • test
  • assert_eq
[package]
name = "test-function"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
fn main() {
    println!("{}", add(2, 3));
}

fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[test]
fn test_add() {
    assert_eq!(add(2, 3), 5);
}

Show STDOUT and STDERR during testing

  • nocapture

In this example there are print-statements both in the code and in the test function.

#![allow(unused)]
fn main() {
pub fn add(left: usize, right: usize) -> usize {
    println!("STDOUT In the application");
    eprintln!("STDERR In the application");
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        println!("STDOUT in the test");
        eprintln!("STDERR in the test");
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}
}

If we run cargo test we don't see any of this as the tester captures them.

If we run cargo test -- --nocapture then we'll see the output of all the 4 print-statements.

Testing with temporary directory passed as an environment variable (test in a single thread RUST_TEST_THREADS)

  • config.toml
  • RUST_TEST_THREADS
  • --test-threads
  • env

Because environment variables are per-process and not per thread, we cannot run the tests in threads (which is the default).

[package]
name = "tempfile-with-environment-variable"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.8.5"
tempfile = "3.9.0"


[alias]
t = "test -- --test-threads=1"

[env]
RUST_TEST_THREADS = "1"


use std::fs::File;
use std::io::Write;

fn main() {}

#[allow(dead_code)]
fn add(x: i32, y: i32) -> i32 {
    let time: u64 = rand::random();
    let time = time % 3;

    std::thread::sleep(std::time::Duration::from_secs(time));

    if let Ok(file_path) = std::env::var("RESULT_PATH") {
        let mut file = File::create(&file_path).unwrap();
        println!("add({x}, {y}) file {file_path}");
        writeln!(&mut file, "{}", x + y).unwrap();
    }
    x + y
}

#[test]
fn test_add_2_3_5() {
    let tmp_dir = tempfile::tempdir().unwrap();
    println!("tmp_dir: {:?}", tmp_dir);
    let file_path = tmp_dir.path().join("result.txt");
    println!("2+3 - file_path {:?}", file_path);
    std::env::set_var("RESULT_PATH", &file_path);

    let result = add(2, 3);
    assert_eq!(result, 5);

    let result = std::fs::read_to_string(file_path).unwrap();
    assert_eq!(result, "5\n");
}

#[test]
fn test_add_4_4_8() {
    let tmp_dir = tempfile::tempdir().unwrap();
    println!("tmp_dir: {:?}", tmp_dir);
    let file_path = tmp_dir.path().join("result.txt");
    println!("4+4 - file_path {:?}", file_path);
    std::env::set_var("RESULT_PATH", &file_path);

    let result = add(4, 4);
    assert_eq!(result, 8);

    let result = std::fs::read_to_string(file_path).unwrap();
    assert_eq!(result, "8\n");
}

Testing with temorary directory passed in a thread-local variable

  • RefCell
  • thread_local!
[package]
name = "tempfile-with-thread-local"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.8.5"
tempfile = "3.9.0"

use std::fs::File;
use std::io::Write;

use std::cell::RefCell;

thread_local! {
    pub static RESULT_PATH: RefCell<String> = const { RefCell::new(String::new()) };
}

fn main() {}

#[allow(dead_code)]
fn add(x: i32, y: i32) -> i32 {
    let time: u64 = rand::random();
    let time = time % 3;

    std::thread::sleep(std::time::Duration::from_secs(time));

    RESULT_PATH.with_borrow(|file_path| {
        let mut file = File::create(file_path).unwrap();
        println!("add({x}, {y}) file {file_path}");
        writeln!(&mut file, "{}", x + y).unwrap();
    });
    x + y
}

#[test]
fn test_add_2_3_5() {
    println!("{:?}", std::thread::current().id());
    let tmp_dir = tempfile::tempdir().unwrap();
    println!("tmp_dir: {:?}", tmp_dir);
    let file_path = tmp_dir.path().join("result.txt");
    println!("2+3 - file_path {:?}", file_path);
    let result_path = file_path.as_os_str().to_str().unwrap();
    RESULT_PATH.with_borrow_mut(|v| v.push_str(result_path));

    let result = add(2, 3);
    assert_eq!(result, 5);

    let result = std::fs::read_to_string(file_path).unwrap();
    assert_eq!(result, "5\n");
}

#[test]
fn test_add_4_4_8() {
    println!("{:?}", std::thread::current().id());

    let tmp_dir = tempfile::tempdir().unwrap();
    println!("tmp_dir: {:?}", tmp_dir);
    let file_path = tmp_dir.path().join("result.txt");
    println!("4+4 - file_path {:?}", file_path);
    let result_path = file_path.as_os_str().to_str().unwrap();
    RESULT_PATH.with_borrow_mut(|v| v.push_str(result_path));

    let result = add(4, 4);
    assert_eq!(result, 8);

    let result = std::fs::read_to_string(file_path).unwrap();
    assert_eq!(result, "8\n");
}

Testing crates

  • TODO
[package]
name = "test-crate"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
//use std::process::Command;
use std::env;

fn main() {
    let folder = get_url();
    println!("{}", folder);
    // get a repo url on the command line
    //
    // create a temporary directory
    // clone the repo there
    // run the tests in the docker
    // capture the output

    //let result = Command::new("docker"
    //        .arg("run --rm --workdir /opt -v$(pwd):/opt -it --user tester rust-test cargo test")
    //        .output()
    //        .expect("failed to execute process");
    //println!("{}", std::str::from_utf8(&result.stdout).unwrap());
    //println!("{}", std::str::from_utf8(&result.stderr).unwrap());
    //println!("{}", result.status);
}

fn get_url() -> String {
    let args: Vec<String> = env::args().collect();
    if args.len() != 2 {
        std::process::exit(1);
    }
    args[1].clone()
}

Parametrization of tests

I could not find parametrization as exists in pytest, but we can fake it by creating a function that accepts the parameters and then creating many test functions calling that function.

#![allow(unused)]
fn main() {
pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);

        let result = add(3, 3);
        assert_eq!(result, 6);
    }

    #[test]
    fn it_works_1_2() {
        test_add(1, 2, 3)
    }
    #[test]
    fn it_works_2_3() {
        test_add(2, 3, 5)
    }

    fn test_add(a: u64, b: u64, expected: u64) {
        let result = add(a, b);
        assert_eq!(result, expected);
    }
}
}

Rust testing setup and teardown fixtures

We can create setup/teardown fixtures using a struct and the Drop trait that will be executed even if the test panics.

#![allow(unused)]
fn main() {
pub fn div(left: u64, right: u64) -> u64 {
    left / right
}

#[cfg(test)]
mod tests {
    use super::*;

    struct Thing {
        name: String,
    }

    impl Thing {
        pub fn new(name: &str) -> Self {
            println!("before {name}");
            Self {
                name: name.to_owned(),
            }
        }
    }

    impl Drop for Thing {
        fn drop(&mut self) {
            println!("after  {}", self.name);
        }
    }

    #[test]
    fn works_2_2() {
        let _thing = Thing::new("2_2");
        let result = div(2, 2);
        assert_eq!(result, 1);
    }

    #[test]
    fn works_2_0() {
        let _thing = Thing::new("2_0");
        let result = div(2, 0);
        assert_eq!(result, 1);
    }

    #[test]
    fn works_4_3() {
        let _thing = Thing::new("4_3");
        let result = div(4, 3);
        assert_eq!(result, 1);
    }
}
}

running 3 tests
before 2_2
after  2_2
test tests::works_2_2 ... ok
before 2_0
before 4_3
after  4_3
test tests::works_4_3 ... ok
after  2_0
test tests::works_2_0 ... FAILED

failures:

failures:
    tests::works_2_0

test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

TOML

Reading a TOML file

[package]
name = "toml-read"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
toml = "0.7.6"
serde = { version = '1.0', features = ['derive'] }
use serde::Deserialize;
use toml::Table;

#[derive(Deserialize)]
#[allow(dead_code)]
struct Config {
    name: String,
    year: u16,
    input: Input,
    output: Output,
    dependencies: Table,
}

#[derive(Deserialize)]
#[allow(dead_code)]
struct Input {
    name: String,
    year: Option<u16>,
}

#[derive(Deserialize)]
#[allow(dead_code)]
struct Output {
    name: String,
    year: Option<u16>,
}

fn main() {
    parse_toml();
}

fn parse_toml() {
    let text = String::from(
        "
name = 'bar'
year = 2023

[input]
name = \"some input\"
year = 1024

[output]
name = 'other thing'

[dependencies]
toml = '0.5'
yaml = '0.9'
serde = { version = '1.0', features = ['derive'] }
    ",
    );

    let config: Config = toml::from_str(&text).unwrap();

    println!("{}", config.name);
    println!("{}", config.year);
    println!("{}", config.input.name);
    match config.input.year {
        Some(value) => println!("input.year: {}", value),
        None => println!("input.year is missing"),
    };
    println!("{}", config.output.name);

    match config.output.year {
        Some(value) => println!("output.year: {}", value),
        None => println!("output.year is missing"),
    };

    //println!("{}", config.dependencies);
    for (k, v) in config.dependencies.iter() {
        println!("{} -> {}", k, v);
    }

    println!("{}", config.dependencies["serde"]["features"][0]);
    println!("{}", config.dependencies["serde"]["version"]);
}

Parsing TOML values

[package]
name = "toml-parse"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
toml = "0.7.6"
use toml::Table;

fn main() {
    parse_toml_pairs();
}

fn parse_toml_pairs() {
    let value = "name = 'bar'\nyear = 2023".parse::<Table>().unwrap();

    assert_eq!(value["name"].as_str(), Some("bar"));
    println!("{}", value["name"].as_str().unwrap());

    assert_eq!(value["year"].as_integer(), Some(2023));
    println!("{}", value["year"].as_integer().unwrap());
}

Execute and run external commands

The external program we'll run

use std::env;
use std::process::exit;

fn main() {
    let mut exit_code = 0;
    let args: Vec<String> = env::args().collect();
    if args.len() == 2 {
        exit_code = args[1].parse().unwrap();
    }

    println!("to stdout");
    eprintln!("to stderr. Exit code {}", exit_code);
    exit(exit_code);
}

Compile:

cargo build --relase

Run:

./target/release/all
./target/release/all 3

Run external programs

  • Command

  • from_utf8

  • status

  • success

  • ExitStatus

  • Command

use std::process::Command;

fn main() {
    check(3);
    check(0);
}

fn check(exit_code: i32) {
    let result = Command::new("../all/target/release/all")
        .arg(format!("{exit_code}"))
        .output()
        .expect("failed to execute process");
    print!("{}", std::str::from_utf8(&result.stdout).unwrap());
    print!("{}", std::str::from_utf8(&result.stderr).unwrap());
    println!("{}", result.status);

    assert_eq!(std::str::from_utf8(&result.stdout).unwrap(), "to stdout\n");
    assert_eq!(
        std::str::from_utf8(&result.stderr).unwrap(),
        format!("to stderr. Exit code {}\n", exit_code)
    );
    if exit_code == 0 {
        assert!(result.status.success());
    }
    assert_eq!(result.status.code().unwrap(), exit_code);
    println!();
}

{% embed include file="src/examples/external/run-external-command/out.out)

see

Run external program combining parameters

  • args
use std::process::Command;

fn main() {
    // ls -l -a -s
    let result = Command::new("ls")
        .args(["-l", "-a", "-s"])
        .output()
        .expect("ls command failed to start");

    println!("stdout:\n{}", std::str::from_utf8(&result.stdout).unwrap());
    println!("stderr:\n{}", std::str::from_utf8(&result.stderr).unwrap());
    println!("{}", result.status);
}

Run a command provided as a string

use std::process::Command;

fn main() {
    let command = "ls -l -a -s";
    let parts: Vec<&str> = command.split(' ').collect();
    //println!("{:?}", parts);

    let cmd = &parts[0];
    let args = &parts[1..parts.len()];

    let result = Command::new(cmd)
        .args(args)
        .output()
        .expect("ls command failed to start");

    println!("stdout:\n{}", std::str::from_utf8(&result.stdout).unwrap());
    println!("stderr:\n{}", std::str::from_utf8(&result.stderr).unwrap());
    println!("{}", result.status);
}

Run external command in another directory

  • current_dir
use std::process::Command;

fn main() {
    let result = Command::new("pwd")
        .output()
        .expect("pwd command failed to start");
    print!("{}", std::str::from_utf8(&result.stdout).unwrap());

    let result = Command::new("pwd")
        .current_dir("src")
        .output()
        .expect("pwd command failed to start");
    print!("{}", std::str::from_utf8(&result.stdout).unwrap());

    let result = Command::new("pwd")
        .current_dir("/etc/")
        .output()
        .expect("pwd command failed to start");
    print!("{}", std::str::from_utf8(&result.stdout).unwrap());

    println!("{}", std::env::current_dir().unwrap().display());
}

{% embed include file="src/examples/external/chdir-for-execution/out.out)